Admin Panel

Resource Definitions

Resources are the building blocks of the Grit admin panel. Define your data model once in TypeScript and get a complete admin interface — data table, forms, filters, sidebar navigation, and dashboard widgets.

The defineResource() API

Every admin resource is created with the defineResource() function. It accepts a single configuration object that describes the resource name, API endpoint, table columns, form fields, and optional dashboard widgets.

ResourceConfig type
interface ResourceConfig {
  // Identity
  name: string              // Singular name, PascalCase (e.g. "Invoice")
  slug?: string             // URL slug, auto-derived if omitted (e.g. "invoices")
  endpoint: string          // Go API base URL (e.g. "/api/invoices")
  icon: string              // Lucide icon name (e.g. "FileText")
  label?: {                 // Display labels (auto-derived from name)
    singular: string        // "Invoice"
    plural: string          // "Invoices"
  }

  // Table configuration
  table: TableConfig

  // Form configuration
  form: FormConfig

  // Dashboard widgets (optional)
  dashboard?: DashboardConfig

  // Access control (optional)
  permissions?: {
    roles: string[]         // Roles that can access this resource
  }
}

Resource Configuration

Name, Slug, and Endpoint

The name field is the singular PascalCase name of your resource (e.g. "Invoice"). Grit auto-derives the plural form, URL slug, and display labels from it. You can override any of these with the slug and label fields.

The endpoint must match the API route registered in your Go backend. For a resource named "Invoice", the endpoint is typically /api/invoices. The admin panel appends /:id for single-item operations automatically.

Icon

Pass any Lucide icon name as a string. The sidebar renders it next to the resource label. Common choices: "Users", "FileText", "ShoppingCart", "CreditCard", "Mail".

Table Configuration

The table object controls how data appears in the resource's data table — which columns are shown, how they are formatted, what filters are available, and which row actions are enabled.

TableConfig type
interface TableConfig {
  columns: ColumnDef[]
  filters?: FilterDef[]
  pageSize?: number            // Default: 20
  defaultSort?: {
    key: string
    direction: 'asc' | 'desc'
  }
  searchable?: boolean         // Enable global search (default: true)
  actions?: Action[]           // 'create' | 'edit' | 'delete' | 'view' | 'export'
  bulkActions?: BulkAction[]   // 'delete' | 'export' | custom string
}

Column Definitions

Each column maps a field from the API response to a table column. The column definition controls sorting, searching, formatting, and custom rendering.

ColumnDef type
interface ColumnDef {
  key: string                  // JSON field name (supports dot notation: "customer.name")
  label: string                // Column header text
  sortable?: boolean           // Allow sorting by this column
  searchable?: boolean         // Include in global search
  format?: ColumnFormat        // Display format
  badge?: BadgeConfig          // Badge-style rendering for status fields
  relation?: string            // Load from a related resource
  hidden?: boolean             // Hidden by default (show/hide toggle)
}

type ColumnFormat =
  | 'text'                     // Plain text (default)
  | 'number'                   // Formatted number (1,234)
  | 'currency'                 // Currency ($1,234.00)
  | 'boolean'                  // Check/X icon
  | 'date'                     // Formatted date (Jan 15, 2026)
  | 'relative'                 // Relative time (3 hours ago)
  | 'badge'                    // Colored badge
  | 'image'                    // Thumbnail image

interface BadgeConfig {
  [value: string]: {
    color: 'green' | 'yellow' | 'red' | 'blue' | 'purple' | 'gray'
    label: string
  }
}

Column Format Types

The format property determines how the cell value is rendered:

FormatInputRendered As
text"Hello World"Hello World
number12345671,234,567
currency99.5$99.50
booleantrue / falseGreen check / Red X icon
date"2026-01-15T..."Jan 15, 2026
relative"2026-01-15T..."3 weeks ago
badge"active"Colored pill with label
image"https://..."32x32 rounded thumbnail

Badge Columns

For status-like fields, use the badge property instead of (or in addition to) format. It maps each possible value to a colored label:

Badge column example
{
  key: 'status',
  label: 'Status',
  badge: {
    paid:    { color: 'green',  label: 'Paid' },
    pending: { color: 'yellow', label: 'Pending' },
    overdue: { color: 'red',    label: 'Overdue' },
  },
}

Filters

Filters appear above the data table and let users narrow down results. Three filter types are available:

FilterDef type
interface FilterDef {
  key: string                  // Field to filter on
  type: 'select' | 'date-range' | 'number-range'
  label?: string               // Display label (defaults to key)
  options?: string[]           // For 'select' type
}

// Example filters
filters: [
  { key: 'status', type: 'select', options: ['paid', 'pending', 'overdue'] },
  { key: 'created_at', type: 'date-range' },
  { key: 'amount', type: 'number-range' },
]

Form Configuration

The form object defines the fields that appear in create and edit modals. Grit supports a wide range of field types and validates input using Zod schemas from the shared package.

FormConfig type
interface FormConfig {
  fields: FieldDef[]
  layout?: 'single' | 'two-column'  // Default: 'single'
  validation?: string               // Zod schema name from shared package
}

interface FieldDef {
  key: string                  // JSON field name
  label: string                // Display label
  type: FieldType              // Input type
  required?: boolean           // Required field (default: false)
  placeholder?: string         // Placeholder text
  default?: any                // Default value for create mode
  options?: string[] | { label: string; value: string }[]
  min?: number                 // For number type
  max?: number                 // For number type
  step?: number                // For number type
  rows?: number                // For textarea type
  span?: 'full' | 'half'      // Column span in two-column layout
}

type FieldType =
  | 'text'
  | 'textarea'
  | 'number'
  | 'select'
  | 'date'
  | 'datetime'
  | 'toggle'
  | 'checkbox'
  | 'radio'
  | 'image'                    // Single image upload
  | 'images'                   // Multiple image upload
  | 'video'                    // Single video upload
  | 'videos'                   // Multiple video upload
  | 'file'                     // Single file upload
  | 'files'                    // Multiple file upload
  | 'richtext'                 // Rich text editor
  | 'relationship-select'      // Select from related resource
  | 'multi-relationship-select' // Multi-select from related resource

Form Field Types

Each field type renders a different input component. Here is a quick reference:

TypeComponentExtra Props
textText inputplaceholder
textareaMulti-line textarearows, placeholder
numberNumeric inputmin, max, step
selectDropdown selectoptions
dateDate picker--
datetimeDate & time picker--
toggleToggle switch--
checkboxCheckbox--
radioRadio button groupoptions
imageSingle image uploadaccept
imagesMultiple image uploadaccept
videoSingle video uploadaccept
videosMultiple video uploadaccept
fileSingle file uploadaccept
filesMultiple file uploadaccept
richtextRich text editor--
relationship-selectResource selectresource, displayKey
multi-relationship-selectResource multi-selectresource, displayKey

Dashboard Configuration

Each resource can optionally define widgets that appear on the admin dashboard. Widgets pull data from your Go API and display stats, charts, or activity feeds.

DashboardConfig type
interface DashboardConfig {
  widgets: WidgetDef[]
}

interface WidgetDef {
  type: 'stat' | 'chart' | 'activity'
  label: string
  query: string                // Server-side query (e.g. "sum:amount")
  format?: 'number' | 'currency' | 'percent'
  color?: string               // Accent color for the widget
  // Chart-specific
  chartType?: 'line' | 'bar'   // For type: 'chart'
}

Complete Example

Here is a full resource definition for a Posts resource with table columns, filters, form fields, and dashboard widgets:

apps/admin/resources/posts.ts
import { defineResource } from '@grit/admin'

export default defineResource({
  name: 'Post',
  endpoint: '/api/posts',
  icon: 'FileText',
  label: {
    singular: 'Blog Post',
    plural: 'Blog Posts',
  },

  table: {
    columns: [
      { key: 'id', label: 'ID', sortable: true, hidden: true },
      { key: 'title', label: 'Title', sortable: true, searchable: true },
      { key: 'author.name', label: 'Author', relation: 'author' },
      { key: 'category', label: 'Category', sortable: true },
      { key: 'status', label: 'Status', badge: {
        published: { color: 'green', label: 'Published' },
        draft:     { color: 'yellow', label: 'Draft' },
        archived:  { color: 'gray', label: 'Archived' },
      }},
      { key: 'views', label: 'Views', format: 'number', sortable: true },
      { key: 'published_at', label: 'Published', format: 'date' },
      { key: 'created_at', label: 'Created', format: 'relative' },
    ],
    filters: [
      { key: 'status', type: 'select', options: ['published', 'draft', 'archived'] },
      { key: 'category', type: 'select', options: ['tech', 'design', 'business'] },
      { key: 'published_at', type: 'date-range' },
    ],
    pageSize: 25,
    defaultSort: { key: 'created_at', direction: 'desc' },
    actions: ['create', 'edit', 'delete', 'export'],
    bulkActions: ['delete', 'export'],
  },

  form: {
    layout: 'two-column',
    fields: [
      { key: 'title', label: 'Title', type: 'text', required: true,
        placeholder: 'Enter post title', span: 'full' },
      { key: 'slug', label: 'Slug', type: 'text',
        placeholder: 'auto-generated-from-title' },
      { key: 'category', label: 'Category', type: 'select',
        options: ['tech', 'design', 'business'] },
      { key: 'content', label: 'Content', type: 'richtext', span: 'full' },
      { key: 'excerpt', label: 'Excerpt', type: 'textarea', rows: 3,
        span: 'full' },
      { key: 'status', label: 'Status', type: 'select',
        options: ['published', 'draft', 'archived'], default: 'draft' },
      { key: 'featured', label: 'Featured Post', type: 'toggle' },
      { key: 'cover_image', label: 'Cover Image', type: 'file', span: 'full' },
    ],
  },

  dashboard: {
    widgets: [
      { type: 'stat', label: 'Total Posts', query: 'count', color: 'purple' },
      { type: 'stat', label: 'Published', query: 'count:status=published',
        color: 'green' },
      { type: 'stat', label: 'Total Views', query: 'sum:views',
        format: 'number' },
      { type: 'chart', label: 'Posts Over Time', chartType: 'line',
        query: 'count:by:month' },
    ],
  },

  permissions: {
    roles: ['admin', 'editor'],
  },
})

Resource Registry

After creating a resource definition file, you need to register it in the resource registry. This file is the single source of truth for all admin resources — the sidebar, router, and dashboard all read from it.

apps/admin/resources/index.ts
import users from './users'
import posts from './posts'
import invoices from './invoices'

// All registered resources — sidebar and routes are generated from this array
export const resources = [users, posts, invoices]

// Helper to look up a resource by slug
export function getResource(slug: string) {
  return resources.find((r) => r.slug === slug)
}

When you run grit generate resource, the CLI automatically adds the import and registration to this file using marker-based code injection. You never need to edit it manually unless you want to reorder the sidebar items.

How Resources Auto-Register in the Sidebar

The admin sidebar component imports the resources array from the registry and renders a navigation link for each one. Each resource'sicon and label.plural (or auto-derived name) are used as the sidebar text. The active item is highlighted based on the current URL path.

The order of resources in the registry array determines their order in the sidebar. System pages (Dashboard, Jobs, Files, Settings) are rendered separately and always appear at fixed positions.