This tutorial walks through building a simple addon that adds a server notes feature. Users can write notes on their servers, and admins can see all notes across all servers.
What we are building
An addon with:
- A database table for notes
- A route that shows notes on the server page
- An admin page that lists all notes
- A sidebar entry for the admin view
Step 1: the directory
mkdir -p panel/storage/addons/server-notes/views
cd panel/storage/addons/server-notes
Step 2: package.json
{
"name": "Server Notes",
"version": "1.0.0",
"description": "Add notes to servers",
"author": "you",
"main": "index.ts",
"router": "/server-notes",
"migrations": [
{
"name": "server_notes_v1_create_table",
"sql": "CREATE TABLE IF NOT EXISTS ServerNotes (id INTEGER PRIMARY KEY AUTOINCREMENT, serverUuid TEXT NOT NULL, userId INTEGER NOT NULL, note TEXT NOT NULL, createdAt TEXT NOT NULL DEFAULT (datetime('now')))"
}
]
}
The migration creates a table with a server UUID, user ID, note text, and timestamp.
Step 3: the entry point
import { Router } from 'express';
import path from 'path';
export default function(router: Router, api: any) {
const { logger, prisma } = api;
// Admin page — list all notes
router.get('/admin', async (req: any, res: any) => {
if (!req.session?.user || !await api.utils.isUserAdmin(req.session.user.id)) {
return res.redirect('/login');
}
try {
const notes = await prisma.$queryRaw`
SELECT sn.*, u.username, s.name as serverName
FROM ServerNotes sn
LEFT JOIN Users u ON sn.userId = u.id
LEFT JOIN Servers s ON sn.serverUuid = s.uuid
ORDER BY sn.createdAt DESC
`;
res.render(path.join(api.viewsPath, 'admin.ejs'), {
user: req.session.user,
req,
notes,
components: {
header: api.getComponentPath('views/components/header'),
template: api.getComponentPath('views/components/template'),
footer: api.getComponentPath('views/components/footer')
}
});
} catch (error) {
logger.error('server-notes admin error', error);
res.status(500).send('something broke');
}
});
// API — add a note
router.post('/api/add', async (req: any, res: any) => {
if (!req.session?.user) return res.status(401).json({ error: 'not logged in' });
const { serverUuid, note } = req.body;
if (!serverUuid || !note) return res.status(400).json({ error: 'missing fields' });
try {
await prisma.$executeRaw`
INSERT INTO ServerNotes (serverUuid, userId, note)
VALUES (${serverUuid}, ${req.session.user.id}, ${note})
`;
res.json({ success: true });
} catch (error) {
logger.error('server-notes add error', error);
res.status(500).json({ error: 'failed to add note' });
}
});
// Add sidebar item
api.ui.addSidebarItem({
id: 'server-notes',
name: 'All Notes',
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>',
link: '/server-notes/admin',
section: 'admin',
order: 60
});
}
Step 4: the admin view
Create views/admin.ejs:
<%- include(components.header, { title: 'Server Notes', user: user }) %>
<main class="h-screen m-auto">
<div class="flex h-screen">
<div class="w-60 h-full">
<%- include(components.template) %>
</div>
<div class="flex-1 p-6 overflow-y-auto pt-16">
<div class="px-8 mt-5">
<h1 class="text-base font-medium text-white">All Server Notes</h1>
<p class="mt-1 text-sm text-neutral-500">Notes added by users on their servers</p>
</div>
<div class="px-8 mt-5">
<% if (notes.length === 0) { %>
<p class="text-neutral-500">No notes yet</p>
<% } else { %>
<div class="space-y-3">
<% notes.forEach(function(note) { %>
<div class="rounded-xl bg-neutral-900 p-4">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-white"><%= note.serverName || 'Unknown' %></span>
<span class="text-xs text-neutral-500"><%= note.createdAt %></span>
</div>
<p class="mt-2 text-sm text-neutral-400"><%= note.note %></p>
<p class="mt-1 text-xs text-neutral-600">by <%= note.username || 'unknown' %></p>
</div>
<% }); %>
</div>
<% } %>
</div>
</div>
</div>
</main>
<%- include(components.footer) %>
Step 5: enable and test
- Restart the panel
- Go to Admin > Addons and enable "Server Notes"
- Visit
/server-notes/adminto see the admin view - Use the API endpoint to add notes programmatically
What you learned
- How to define a migration in
package.json - How to create routes with Express
- How to use
$queryRawand$executeRawfor addon tables - How to register a sidebar item
- How to render views with the panel's layout components
From here you can add server-specific note views, subuser permissions, or integrate with the console to add notes from the terminal.