Addon Development
SDK Contracts & Manifest
Deep dive into contracts, permissions, and UI extension mount point definitions.
Defining the Addon Manifest
The manifest declares addon identification, compatibility ranges, UI mount points, server bundles, and role-based permissions in src/manifest.ts:
import type { AddonManifest } from "@jchat/addon-sdk"
const manifest: AddonManifest = {
id: "pollwise",
name: "Pollwise",
version: "1.0.0",
description: "Create polls and gather votes from users in the chat.",
author: "JChat Team",
publisher: { id: "jchat" },
compatibility: {
jchat: ">=1.0.0 <2.0.0",
addonSdk: "^1.0.0"
},
locales: [
"ar",
"bg",
"de",
"el",
"en",
"es",
"fr",
"he",
"hi",
"hr",
"kn",
"ml",
"nl",
"pt",
"ro",
"ru",
"ta",
"te",
"tr",
"ur"
],
defaultLocale: "en",
frontend: {
mountPoints: ["chat-toolbar", "chat-plus-menu", "owner-settings", "addon-settings"],
bundle: "./ui.js"
},
server: {
bundle: "./api.ts",
migrations: ["migrations/001_create_polls.sql"]
},
accessPermission: "addon.pollwise.vote",
permissions: ["addon.pollwise.vote", "addon.pollwise.create", "addon.pollwise.manage"],
permissionDefinitions: [
{
name: "addon.pollwise.vote",
label: "Vote in Polls",
description: "Vote in polls"
},
{
name: "addon.pollwise.create",
label: "Create Polls",
description: "Create polls in the chat"
},
{
name: "addon.pollwise.manage",
label: "Manage Polls",
description: "Close or delete any poll"
}
]
}
export default manifestFrontend UI Component
The frontend React entrypoint lives in src/ui.tsx. It receives AddonProps (mountPoint, siteId, roomId) and uses @jchat/addon-sdk hooks:
import {
MOUNT_POINTS,
useAddonSDK,
useAddonQuery,
useAddonMutation,
useTranslation,
type AddonProps
} from "@jchat/addon-sdk"
import Icons from "@jchat/addon-sdk/icon"
import {
Button,
Input,
Label,
Popover,
PopoverContent,
PopoverTrigger,
Switch
} from "@jchat/addon-sdk/ui"
import locales from "./locales"
import "./tailwind.css"
export default function Pollwise({ mountPoint, siteId, roomId }: AddonProps) {
const sdk = useAddonSDK()
const { t, dir } = useTranslation(locales)
// 1. Render Admin Settings panels
if (mountPoint === "owner-settings" || mountPoint === "addon-settings") {
return <PollwiseSettings siteId={siteId} />
}
// 2. Fetch data via TanStack Query bridge scoped to /api/v1/addons/<addonId>
const { data, isLoading } = useAddonQuery<{ data: any[] }>({
path: "/polls"
})
// 3. Render Composer Toolbar / Plus Menu action
return (
<Popover>
<PopoverTrigger asChild>
<button
aria-label={t("addon_title")}
className="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground"
>
<Icons.BarChart3 className="h-4 w-4" />
</button>
</PopoverTrigger>
<PopoverContent className="w-80 p-4">
<h4 className="font-semibold text-foreground">{t("create_poll")}</h4>
{/* Interactive Poll Form */}
</PopoverContent>
</Popover>
)
}Supported UI Mount Points
Addons can inject React components into multiple layout slots declared in their manifest:
| Mount Point | Location | Typical Use Case |
|---|---|---|
chat-toolbar | Composer toolbar | Quick action buttons (Polls, Giphy, Soundboard) |
chat-plus-menu | Composer (+) menu | Extended tools, games, dice rolls, attachments |
chat-sidebar | Right chat drawer | Interactive feeds, participant widgets, active games |
chat-header | Room top header | Channel badges, livestream links, status alerts |
owner-settings | Owner Control Panel | Master credentials, global API keys, licensing |
addon-settings | Site Admin Dashboard | Site-level preferences and feature switches |
emoji-picker-gif-tab | Emoji/Media modal | Dedicated GIF search tabs (GifYard) |
emoji-picker-sticker-tab | Emoji/Media modal | Custom sticker collections (Stickerly) |
message-context-menu | Message dropdown | Message translation, bookmarking, moderation |
Backend Server Factory (src/api.ts)
Backend-bearing addons export a default factory function that receives AddonServerContext and an Elysia AddonApp instance:
import { verifyAddonEntitlement, type AddonApp, type AddonServerContext } from "@jchat/addon-sdk"
import type { MyAddonDatabase } from "./db"
export default (
{
db,
cache,
emit,
t,
verifyEntitlement,
getConfig,
getUser,
hasPermission
}: AddonServerContext<MyAddonDatabase>,
app: AddonApp
) => {
// Commercial addons: verify entitlement directly
verifyAddonEntitlement("my-addon", verifyEntitlement)
return app.get("/items", async ({ user }) => {
const items = await db
.selectFrom("my_addon_items")
.where("siteId", "=", user?.siteId ?? "")
.selectAll()
.execute()
return { items }
})
}AddonServerContext Members
| Member | Type | Description |
|---|---|---|
db | AddonDatabase<T> | Type-safe, schema-scoped query builder. Tables live in "addons" and use the <addonId>_ prefix. |
verifyEntitlement | (id?: string) => boolean | Verifies that the host holds an active commercial license entitlement for this addon. |
cache | AddonCacheClient | Namespaced Redis key-value cache client (get, set, del, incr, expire, ttl). |
emit | (event, payload, target?) => Promise<void> | Real-time WebSocket event broadcaster across cluster rooms or sites. |
t | typeof t | Host-injected TypeBox schema builder for validating body, query, params, and headers. |
getConfig | (siteId: string) => Promise<Record<string, unknown>> | Retrieves configured addon settings for a given site. |
getUser | (userId: string) => Promise<AddonUser | null> | Retrieves public profile data for a specific user. |
getUsers | (userIds: string[]) => Promise<Map<string, AddonUser>> | Batch-fetches multiple user profiles in a single query. |
hasPermission | (user, permission) => boolean | Checks whether the request user holds a given JChat or custom addon permission. |