Tutorial: Your First Plugin
Build a simple "Notes" plugin in 30 minutes.
What We'll Build
A plugin that:
- Shows a feed of notes
- Lets users create and edit notes
- Stores notes in Weaviate
- Displays a widget on the dashboard
Prerequisites
- Node.js 18+
- Python 3.11+
- pnpm installed
- Repository cloned and dependencies installed
Step 1: Create Plugin Directory
mkdir -p apps/front/plugins/notes/components
Step 2: Create the Manifest
// apps/front/plugins/notes/manifest.ts
import type { PluginManifest } from '@/lib/plugins/types';
export const manifest: PluginManifest = {
id: 'notes',
name: 'Notes',
description: 'Simple note-taking plugin',
icon: 'StickyNote',
version: '1.0.0',
author: 'Your Name',
nav: {
label: 'Notes',
icon: 'StickyNote',
views: [
{
id: 'feed',
label: 'All Notes',
path: '/feed?plugin=notes.feed',
icon: 'List',
},
{
id: 'new',
label: 'New Note',
path: '/write?plugin=notes.editor&new=true',
icon: 'Plus',
},
],
},
shells: {
feed: {
callable: 'list_notes',
card: 'NoteCard',
detail: 'NoteDetail',
filters: ['date'],
},
write: {
callable: 'editor',
views: ['editor'],
},
},
customViews: {
editor: {
component: 'NoteEditor',
title: 'Note Editor',
},
},
widget: {
component: 'NotesWidget',
defaultSize: 'small',
refreshInterval: 60000,
},
};
Step 3: Create the Index File
// apps/front/plugins/notes/index.ts
export { manifest } from './manifest';
// Components
export { NoteCard } from './components/NoteCard';
export { NoteDetail } from './components/NoteDetail';
export { NoteEditor } from './components/NoteEditor';
export { NotesWidget } from './components/NotesWidget';
Step 4: Create the Card Component
// apps/front/plugins/notes/components/NoteCard.tsx
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { formatDistanceToNow } from 'date-fns';
interface NoteCardProps {
item: {
id: string;
content: {
title: string;
body: string;
};
metadata: {
created_at: string;
};
};
onClick: () => void;
}
export function NoteCard({ item, onClick }: NoteCardProps) {
return (
<Card
className="cursor-pointer hover:shadow-md transition-shadow"
onClick={onClick}
>
<CardHeader className="pb-2">
<CardTitle className="text-lg">{item.content.title}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground line-clamp-2">
{item.content.body}
</p>
<p className="text-xs text-muted-foreground mt-2">
{formatDistanceToNow(new Date(item.metadata.created_at), { addSuffix: true })}
</p>
</CardContent>
</Card>
);
}
Step 5: Create the Detail Component
// apps/front/plugins/notes/components/NoteDetail.tsx
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Input } from '@/components/ui/input';
import { useState } from 'react';
interface NoteDetailProps {
item: {
id: string;
content: {
title: string;
body: string;
};
};
onClose: () => void;
onUpdate: (data: { content: { title: string; body: string } }) => void;
}
export function NoteDetail({ item, onClose, onUpdate }: NoteDetailProps) {
const [title, setTitle] = useState(item.content.title);
const [body, setBody] = useState(item.content.body);
const handleSave = () => {
onUpdate({ content: { title, body } });
};
return (
<div className="space-y-4 p-4">
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Note title"
className="text-xl font-semibold"
/>
<Textarea
value={body}
onChange={(e) => setBody(e.target.value)}
placeholder="Write your note..."
className="min-h-[300px]"
/>
<div className="flex gap-2">
<Button onClick={handleSave}>Save</Button>
<Button variant="outline" onClick={onClose}>Close</Button>
</div>
</div>
);
}
Step 6: Create the Widget
// apps/front/plugins/notes/components/NotesWidget.tsx
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { StickyNote } from 'lucide-react';
import Link from 'next/link';
interface NotesWidgetProps {
data: {
recentNotes: Array<{ id: string; content: { title: string } }>;
totalCount: number;
};
}
export function NotesWidget({ data }: NotesWidgetProps) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">Notes</CardTitle>
<StickyNote className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{data.totalCount}</div>
<p className="text-xs text-muted-foreground">total notes</p>
<div className="mt-4 space-y-1">
{data.recentNotes.slice(0, 3).map((note) => (
<Link
key={note.id}
href={`/feed?plugin=notes.feed&id=${note.id}`}
className="block text-sm text-blue-600 hover:underline truncate"
>
{note.content.title}
</Link>
))}
</div>
</CardContent>
</Card>
);
}
Step 7: Create Backend Handler
# packages/plugins/installed/notes/handler.py
from datetime import datetime
from typing import Optional, List, Dict, Any
from services.weaviate.plugin_repository import PluginRepository
repo = PluginRepository()
PLUGIN_ID = "notes"
def list_notes(user_id: str, filters: Optional[Dict] = None) -> List[Dict]:
"""List all notes for a user."""
return repo.list_by_user(
plugin_id=PLUGIN_ID,
user_id=user_id,
data_type="note",
filters=filters,
sort_by="created_at",
sort_order="desc",
)
def get_note(user_id: str, note_id: str) -> Optional[Dict]:
"""Get a single note."""
return repo.get(plugin_id=PLUGIN_ID, item_id=note_id)
def create_note(user_id: str, title: str, body: str) -> Dict:
"""Create a new note."""
return repo.create(
plugin_id=PLUGIN_ID,
user_id=user_id,
data_type="note",
content={"title": title, "body": body},
metadata={"created_at": datetime.utcnow().isoformat()},
)
def update_note(user_id: str, note_id: str, title: str, body: str) -> Dict:
"""Update an existing note."""
return repo.update(
plugin_id=PLUGIN_ID,
item_id=note_id,
content={"title": title, "body": body},
metadata={"updated_at": datetime.utcnow().isoformat()},
)
def delete_note(user_id: str, note_id: str) -> bool:
"""Delete a note."""
return repo.delete(plugin_id=PLUGIN_ID, item_id=note_id)
def get_widget_data(user_id: str) -> Dict:
"""Get data for the dashboard widget."""
notes = list_notes(user_id)
return {
"recentNotes": notes[:5],
"totalCount": len(notes),
}
Step 8: Register the Plugin
// apps/front/lib/plugins/registry.ts
import { manifest as notes } from '@/plugins/notes/manifest';
export const pluginRegistry = {
// ... existing plugins
notes,
};
Step 9: Test Your Plugin
# Start the development server
cd apps/front && pnpm dev
# Navigate to your plugin
# http://localhost:3000/feed?plugin=notes.feed
Congratulations!
You've built your first DebaterHub plugin. From here, you can:
- Add search functionality using Weaviate embeddings
- Connect to the Persona Agent for AI features
- Add more complex UI components
- Create scheduled background jobs
Next Steps
- API Reference - All available endpoints
- Jobs Plugin Example - A more complex plugin
- Plugin Architecture - Deep dive into the system