Features
Composable feature components for SaaS applications
Features
The Features system provides high-level feature components that compose multiple views and panels into complete functional areas of your application. Features auto-wire to Payload CMS collections and implement common SaaS patterns for developer portals, team management, billing, and more.
Type Signature
interface FeatureComponent {
/** Feature configuration */
config?: FeatureConfig
/** Auto-wire to Payload collection */
collection?: string
/** Child content/views */
children?: React.ReactNode
}
interface FeatureConfig {
/** Show/hide individual views */
[key: string]: boolean | string | number | object
}DeveloperDashboard Feature
The DeveloperDashboard feature provides a complete developer portal with API keys, webhooks, usage metrics, and request logs.
Type Definition
From app-types.ts:
interface DeveloperAppConfig {
/** Show API keys section */
showAPIKeys?: boolean // default: true
/** Show webhooks section */
showWebhooks?: boolean // default: true
/** Show usage metrics */
showUsage?: boolean // default: true
/** Show API logs */
showLogs?: boolean // default: true
/** Show documentation */
showDocs?: boolean // default: true
/** API base URL */
apiBaseUrl?: string
/** Available API versions */
apiVersions?: string[]
}Usage
<App name="Developer Portal">
<Shell>
<DeveloperDashboard
config={{
showAPIKeys: true,
showWebhooks: true,
showUsage: true,
showLogs: true,
apiBaseUrl: 'https://api.example.com',
apiVersions: ['v1', 'v2']
}}
/>
</Shell>
</App>Included Views
The DeveloperDashboard feature includes:
| View | Route | Description | Auto-wire Collection |
|---|---|---|---|
| Overview | / | Dashboard with API metrics | analytics |
| API Keys | /keys | Manage API authentication keys | apiKeys |
| Webhooks | /webhooks | Configure webhook endpoints | webhooks |
| Usage | /usage | API usage metrics and analytics | usage |
| Logs | /logs | API request/response logs | logs |
Example Implementation
import { DeveloperDashboard } from '@mdxui/cockpit'
// Full feature with all views
<DeveloperDashboard
config={{
showAPIKeys: true,
showWebhooks: true,
showUsage: true,
showLogs: true
}}
/>
// Minimal configuration (keys only)
<DeveloperDashboard
config={{
showAPIKeys: true,
showWebhooks: false,
showUsage: false,
showLogs: false
}}
/>Auto-Wiring
The DeveloperDashboard automatically queries these Payload collections:
// Auto-wired collections
apiKeys // API authentication keys
webhooks // Webhook endpoints
usage // Usage metrics by key/endpoint
logs // Request/response logsEach view component handles its own data fetching via the DataProvider.
Team Management Feature
The Team feature provides complete team member management with roles, permissions, and invitations.
Type Definition
From app-types.ts:
interface SaaSAppConfig {
/** Show team management */
showTeam?: boolean // default: true
/** Enable multi-tenancy */
multiTenant?: boolean // default: true
/** Show workspace switcher */
showWorkspaceSwitcher?: boolean // default: true
}Usage
<App name="Acme SaaS">
<Shell>
<TeamManagement
config={{
showInvites: true,
showRoles: true,
showPermissions: true,
multiTenant: true
}}
/>
</Shell>
</App>Included Components
| Component | Purpose | Integration |
|---|---|---|
| UsersTable | List team members | WorkOS Users Management widget |
| InviteDialog | Send team invitations | WorkOS invitations API |
| RolesManager | Manage user roles | WorkOS roles |
| PermissionsEditor | Configure permissions | WorkOS permissions |
WorkOS Integration
The Team feature integrates with WorkOS for enterprise-ready user management:
import { UsersManagement } from '@workos-inc/widgets'
import { useAuth } from '@workos-inc/authkit-react'
export function TeamPage() {
const { getAccessToken, organizationId } = useAuth()
return (
<div>
<h1>Team</h1>
<p>Manage your team members and their permissions.</p>
<UsersManagement
authToken={getAccessToken}
organizationId={organizationId}
/>
</div>
)
}Permission Checks
Team management requires specific permissions:
const canManageTeam =
hasOrganization &&
permissions?.includes('widgets:users-table:manage')Empty States
Handle different states appropriately:
{!user ? (
<EmptyState
message="Please sign in to manage your team."
/>
) : !hasOrganization ? (
<EmptyState
message="You need to be part of an organization to manage team members."
/>
) : !canManageTeam ? (
<EmptyState
message="You need admin permissions to manage team members."
/>
) : (
<UsersManagement authToken={getAccessToken} />
)}Billing Management Feature
The Billing feature provides subscription management, usage tracking, payment methods, and invoice history.
Type Definition
From app-types.ts:
interface SaaSAppConfig {
/** Show billing section */
showBilling?: boolean // default: true
/** Pricing tiers */
pricingTiers?: string[] // e.g., ['free', 'pro', 'enterprise']
}Usage
<App name="Acme SaaS">
<Shell>
<BillingManagement
config={{
showSubscription: true,
showUsage: true,
showInvoices: true,
showPaymentMethods: true,
pricingTiers: ['starter', 'professional', 'enterprise']
}}
/>
</Shell>
</App>Billing Sections
The Billing feature includes these sections:
1. Current Plan
Display subscription details:
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Current Plan</CardTitle>
<Badge variant={status === 'active' ? 'default' : 'secondary'}>
{status}
</Badge>
</div>
<CardDescription>
Renews {formatDate(renewalDate)}
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-baseline gap-2">
<p className="text-3xl font-bold">{planName}</p>
<p className="text-lg text-muted-foreground">
${planPrice}/mo
</p>
</div>
<div className="space-y-2">
{features.map(feature => (
<div className="flex items-center gap-2">
<Check className="h-4 w-4 text-primary" />
{feature}
</div>
))}
</div>
<Button variant="outline" className="w-full">
Manage Plan
</Button>
</CardContent>
</Card>2. Usage Breakdown
Track resource consumption:
<Card>
<CardHeader>
<CardTitle>Usage Breakdown</CardTitle>
<CardDescription>
Resource usage for the current billing period
</CardDescription>
</CardHeader>
<CardContent>
{usageBreakdown.map(item => {
const percentage = (item.usage / item.limit) * 100
return (
<div key={item.resource} className="space-y-2">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">{item.resource}</p>
<p className="text-sm text-muted-foreground">
{formatUsage(item.usage, item.unit)} of {formatUsage(item.limit, item.unit)}
</p>
</div>
<div className="text-right">
<p className="font-medium">${item.cost.toFixed(2)}</p>
<p className="text-sm text-muted-foreground">
{percentage.toFixed(0)}% used
</p>
</div>
</div>
<Progress
value={percentage}
className={getUsageColor(percentage)}
/>
</div>
)
})}
</CardContent>
</Card>3. Payment Method
Manage payment information:
<Card>
<CardHeader>
<CardTitle>
<CreditCard className="h-4 w-4" />
Payment Method
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between p-3 border rounded-md">
<div className="flex items-center gap-3">
<div className="h-8 w-12 bg-muted rounded flex items-center justify-center">
{brand}
</div>
<div>
<p className="font-medium">•••• {last4}</p>
<p className="text-xs text-muted-foreground">
Expires {expiryMonth}/{expiryYear}
</p>
</div>
</div>
<Badge variant="secondary">Default</Badge>
</div>
<Button variant="outline" className="w-full">
Update
</Button>
</CardContent>
</Card>4. Billing Alerts
Configure usage notifications:
<Card>
<CardHeader>
<CardTitle>
<Bell className="h-4 w-4" />
Billing Alerts
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<Label htmlFor="alerts">Usage alerts</Label>
<p className="text-xs text-muted-foreground">
Notify at {threshold}% usage
</p>
</div>
<Switch
id="alerts"
checked={alertsEnabled}
onCheckedChange={handleAlertsToggle}
/>
</div>
</CardContent>
</Card>5. Invoice History
Display past invoices:
<Card>
<CardHeader>
<CardTitle>
<Receipt className="h-5 w-5" />
Invoice History
</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Date</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Status</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map(invoice => (
<TableRow key={invoice.id}>
<TableCell className="font-mono">
{invoice.invoiceNumber}
</TableCell>
<TableCell>{formatDate(invoice.date)}</TableCell>
<TableCell className="font-medium">
${invoice.amount.toFixed(2)}
</TableCell>
<TableCell>
<Badge variant={invoice.status === 'paid' ? 'default' : 'secondary'}>
{invoice.status}
</Badge>
</TableCell>
<TableCell>
<Button
variant="ghost"
size="icon"
onClick={() => handleDownloadInvoice(invoice.invoiceNumber)}
>
<Download className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>Usage Helpers
Utility functions for formatting billing data:
// Format usage numbers
function formatUsage(usage: number, unit: string): string {
if (usage >= 1000) {
return `${(usage / 1000).toFixed(1)}k ${unit}`
}
return `${usage} ${unit}`
}
// Calculate usage percentage
function getUsagePercentage(usage: number, limit: number): number {
return Math.min((usage / limit) * 100, 100)
}
// Get color based on usage
function getUsageColor(percentage: number): string {
if (percentage >= 90) return 'bg-destructive'
if (percentage >= 75) return 'bg-yellow-500'
return ''
}
// Format dates
function formatRelativeDate(dateString: string): string {
const date = new Date(dateString)
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
})
}Settings Sections Feature
The Settings feature provides comprehensive application configuration organized into logical sections.
Type Definition
interface SettingsConfig {
/** Settings sections to show */
sections?: SettingsSection[]
/** Default section */
defaultSection?: string
}
interface SettingsSection {
id: string
label: string
icon?: string
component: React.ComponentType
}Common Settings Sections
1. General Settings
<SettingsSection id="general" label="General">
<Card>
<CardHeader>
<CardTitle>Organization Name</CardTitle>
<CardDescription>
Update your organization's display name
</CardDescription>
</CardHeader>
<CardContent>
<Input value={orgName} onChange={handleNameChange} />
</CardContent>
</Card>
</SettingsSection>2. Profile Settings
<SettingsSection id="profile" label="Profile">
<Card>
<CardHeader>
<CardTitle>Personal Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<FormField label="Name" name="name" />
<FormField label="Email" name="email" type="email" />
<FormField label="Avatar" name="avatar" type="file" />
</CardContent>
</Card>
</SettingsSection>3. Security Settings
<SettingsSection id="security" label="Security">
<Card>
<CardHeader>
<CardTitle>Two-Factor Authentication</CardTitle>
<CardDescription>
Add an extra layer of security to your account
</CardDescription>
</CardHeader>
<CardContent>
<Switch
checked={twoFactorEnabled}
onCheckedChange={handleToggle2FA}
/>
</CardContent>
</Card>
</SettingsSection>4. Notifications Settings
<SettingsSection id="notifications" label="Notifications">
<Card>
<CardHeader>
<CardTitle>Email Notifications</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<Label>Product updates</Label>
<Switch />
</div>
<div className="flex items-center justify-between">
<Label>Security alerts</Label>
<Switch defaultChecked />
</div>
<div className="flex items-center justify-between">
<Label>Billing notifications</Label>
<Switch defaultChecked />
</div>
</CardContent>
</Card>
</SettingsSection>5. API Settings
<SettingsSection id="api" label="API">
<Card>
<CardHeader>
<CardTitle>API Configuration</CardTitle>
<CardDescription>
Manage API access and webhooks
</CardDescription>
</CardHeader>
<CardContent>
<Link to="/api-keys">
<Button variant="outline" className="w-full">
Manage API Keys
</Button>
</Link>
<Link to="/webhooks">
<Button variant="outline" className="w-full">
Configure Webhooks
</Button>
</Link>
</CardContent>
</Card>
</SettingsSection>Settings Layout
Use a sidebar navigation for multiple sections:
<SettingsView layout="column">
<div className="flex gap-6">
{/* Sidebar */}
<nav className="w-48 space-y-1">
{sections.map(section => (
<Button
key={section.id}
variant={activeSection === section.id ? 'secondary' : 'ghost'}
className="w-full justify-start"
onClick={() => setActiveSection(section.id)}
>
{section.icon && <Icon name={section.icon} />}
{section.label}
</Button>
))}
</nav>
{/* Content */}
<div className="flex-1">
{renderSection(activeSection)}
</div>
</div>
</SettingsView>Auto-Wiring to Payload Collections
Features automatically connect to Payload CMS collections for data management.
Collection Mapping
Features map to Payload collections:
const FEATURE_COLLECTIONS = {
DeveloperDashboard: {
apiKeys: 'apiKeys',
webhooks: 'webhooks',
usage: 'usage',
logs: 'logs'
},
TeamManagement: {
users: 'users',
invites: 'invites',
roles: 'roles',
permissions: 'permissions'
},
BillingManagement: {
subscriptions: 'subscriptions',
invoices: 'invoices',
paymentMethods: 'paymentMethods',
usage: 'usage'
}
}DataProvider Pattern
Features use the DataProvider to fetch collection data:
import { useDataProvider } from '@mdxui/core'
export function APIKeysView() {
const dataProvider = useDataProvider()
const [keys, setKeys] = useState([])
useEffect(() => {
dataProvider.getList('apiKeys', {
pagination: { page: 1, perPage: 10 },
sort: { field: 'created', order: 'DESC' }
}).then(({ data }) => setKeys(data))
}, [])
return (
<div>
{keys.map(key => (
<KeyCard key={key.id} {...key} />
))}
</div>
)
}Explicit Collection Override
Override the default collection:
<DeveloperDashboard
config={{
showAPIKeys: true,
apiKeysCollection: 'custom_api_keys' // Override default
}}
/>Feature Composition Patterns
Compose multiple features in your application:
1. Side-by-Side Features
Display features in columns:
<App name="Full Stack Dashboard">
<Shell>
<View layout="column">
<div className="grid grid-cols-2 gap-6">
<DeveloperDashboard
config={{ showAPIKeys: true, showWebhooks: true }}
/>
<TeamManagement
config={{ showInvites: true }}
/>
</div>
</View>
</Shell>
</App>2. Tabbed Features
Organize features in tabs:
<App name="Admin Portal">
<Shell>
<Tabs defaultValue="developer">
<TabsList>
<TabsTrigger value="developer">Developer</TabsTrigger>
<TabsTrigger value="team">Team</TabsTrigger>
<TabsTrigger value="billing">Billing</TabsTrigger>
</TabsList>
<TabsContent value="developer">
<DeveloperDashboard />
</TabsContent>
<TabsContent value="team">
<TeamManagement />
</TabsContent>
<TabsContent value="billing">
<BillingManagement />
</TabsContent>
</Tabs>
</Shell>
</App>3. Routed Features
Use React Router for feature navigation:
import { Routes, Route } from 'react-router-dom'
<App name="SaaS Platform">
<Shell>
<Routes>
<Route path="/" element={<DashboardView />} />
<Route path="/api/*" element={<DeveloperDashboard />} />
<Route path="/team" element={<TeamManagement />} />
<Route path="/billing" element={<BillingManagement />} />
<Route path="/settings/*" element={<SettingsView />} />
</Routes>
</Shell>
</App>4. Conditional Features
Show features based on permissions:
<App name="Multi-Tenant SaaS">
<Shell>
{hasPermission('developer:view') && (
<DeveloperDashboard />
)}
{hasPermission('team:manage') && (
<TeamManagement />
)}
{hasPermission('billing:view') && (
<BillingManagement />
)}
</Shell>
</App>5. Nested Features
Embed features within other features:
<App name="Enterprise Platform">
<Shell>
<DashboardView>
<div className="grid gap-6">
{/* Quick access to features */}
<Card>
<CardHeader>
<CardTitle>API Overview</CardTitle>
</CardHeader>
<CardContent>
<DeveloperDashboard
config={{
showAPIKeys: true,
showWebhooks: false,
compact: true
}}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Team Status</CardTitle>
</CardHeader>
<CardContent>
<TeamManagement
config={{
showInvites: true,
compact: true
}}
/>
</CardContent>
</Card>
</div>
</DashboardView>
</Shell>
</App>Feature Extensions
Extend features with custom views:
import { DeveloperDashboard } from '@mdxui/cockpit'
// Extend with custom view
<DeveloperDashboard
config={{
showAPIKeys: true,
showWebhooks: true
}}
>
{/* Add custom views */}
<CustomAnalyticsView />
<CustomReportsView />
</DeveloperDashboard>Best Practices
1. Progressive Disclosure
Start with essential features, add more as needed:
// Good - Start simple
<DeveloperDashboard
config={{
showAPIKeys: true,
showWebhooks: false, // Add later when needed
showLogs: false
}}
/>2. Consistent Navigation
Place related features together:
<Sidebar>
<NavSection label="Developer">
<NavItem href="/api-keys">API Keys</NavItem>
<NavItem href="/webhooks">Webhooks</NavItem>
<NavItem href="/usage">Usage</NavItem>
</NavSection>
<NavSection label="Organization">
<NavItem href="/team">Team</NavItem>
<NavItem href="/billing">Billing</NavItem>
</NavSection>
</Sidebar>3. Permission Gating
Always check permissions before showing features:
{hasPermission('billing:view') ? (
<BillingManagement />
) : (
<EmptyState
message="You don't have access to billing."
/>
)}4. Loading States
Handle async data loading:
{isLoading ? (
<FeatureSkeleton />
) : (
<DeveloperDashboard />
)}5. Error Handling
Gracefully handle feature errors:
<ErrorBoundary
fallback={<FeatureError message="Failed to load developer dashboard" />}
>
<DeveloperDashboard />
</ErrorBoundary>