Backend Services & Real-Time Events
Build Elysia API endpoints, manage isolated Kysely databases, and broadcast WebSocket events.
Elysia Backend Router (src/api.ts)
Fullstack addons define their backend in src/api.ts. The host passes an AddonServerContext containing a scoped Kysely database instance, Redis cache, real-time emitter, validation schema builder t, and entitlement verifier:
import { verifyAddonEntitlement, type AddonApp, type AddonServerContext } from "@jchat/addon-sdk"
import type { PollwiseDatabase } from "./db"
export default (
{ db, cache, emit, t, verifyEntitlement }: AddonServerContext<PollwiseDatabase>,
app: AddonApp
) => {
// 1. Verify commercial license entitlement (required for commercial addons)
verifyAddonEntitlement("pollwise", verifyEntitlement)
return app
.get(
"/polls",
async ({ user }) => {
// Direct type-safe Kysely query: camelCase properties map to snake_case DB columns
const polls = await db
.selectFrom("pollwise_polls")
.where("siteId", "=", user?.siteId ?? "")
.where("closedAt", "is", null)
.selectAll()
.execute()
return { status: "success", data: polls }
},
{
requirePermission: "addon.pollwise.vote"
}
)
.post(
"/polls",
async ({ body, user }) => {
const id = Bun.randomUUIDv7()
// 2. Insert into isolated "addons" PostgreSQL schema
await db
.insertInto("pollwise_polls")
.values({
id,
siteId: user?.siteId ?? "",
creatorId: user.id,
question: body.question,
options: body.options,
multipleChoice: body.multipleChoice ?? false
})
.execute()
// 3. Set namespaced Redis cache with TTL
await cache.set(`latest-poll:${user.siteId}`, id, 300)
// 4. Broadcast real-time WebSocket event across site
await emit(
"addon:pollwise:created",
{ pollId: id, question: body.question },
{ siteId: user.siteId }
)
return { status: "success", data: { id } }
},
{
requirePermission: "addon.pollwise.create",
body: t.Object({
question: t.String({ minLength: 1, maxLength: 200 }),
options: t.Array(t.Object({ text: t.String() })),
multipleChoice: t.Optional(t.Boolean())
})
}
)
}Commercial License Verification
Commercial marketplace addons verify their active license on startup using verifyAddonEntitlement. You can pass either the verifyEntitlement function directly from context or the entire context object:
// Pass verifyEntitlement directly when destructuring context:
export default ({ db, verifyEntitlement }: AddonServerContext<MyDb>, app: AddonApp) => {
verifyAddonEntitlement("my-addon", verifyEntitlement)
// ...
}If an addon requires a commercial license and the current deployment lacks a valid entitlement, verifyAddonEntitlement prevents the extension from activating.
Database Schema (src/db.ts)
Database interfaces are declared in src/db.ts using type-safe table definitions. In TypeScript, properties use idiomatic camelCase, which maps automatically to database snake_case columns with 0 runtime bundle overhead:
import type { Generated, Selectable } from "@jchat/addon-sdk"
export interface PollTable {
id: string
siteId: string
roomId: string | null
creatorId: string
question: string
options: { text: string }[]
multipleChoice: boolean
expiresAt: Date | null
closedAt: Date | null
createdAt: Generated<Date>
}
export interface PollVoteTable {
id: string
pollId: string
userId: string
optionIndex: number
createdAt: Generated<Date>
}
export interface PollwiseDatabase {
pollwise_polls: PollTable
pollwise_votes: PollVoteTable
}
export type Poll = Selectable<PollTable>
export type PollVote = Selectable<PollVoteTable>-- migrations/001_initial.sql
CREATE TABLE IF NOT EXISTS "addons"."pollwise_polls" (
"id" VARCHAR(36) PRIMARY KEY,
"site_id" VARCHAR(36) NOT NULL,
"room_id" VARCHAR(36),
"creator_id" VARCHAR(36) NOT NULL,
"question" TEXT NOT NULL,
"options" JSONB NOT NULL DEFAULT '[]',
"multiple_choice" BOOLEAN NOT NULL DEFAULT FALSE,
"expires_at" TIMESTAMPTZ,
"closed_at" TIMESTAMPTZ,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS "addons"."pollwise_votes" (
"id" VARCHAR(36) PRIMARY KEY,
"poll_id" VARCHAR(36) NOT NULL,
"user_id" VARCHAR(36) NOT NULL,
"option_index" INT NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS "idx_pollwise_polls_site" ON "addons"."pollwise_polls" ("site_id");
CREATE INDEX IF NOT EXISTS "idx_pollwise_votes_poll" ON "addons"."pollwise_votes" ("poll_id");20-Language Translation Parity
All JChat addons must support all 20 host languages (ar, bg, de, el, en, es, fr, he, hi, hr, kn, ml, nl, pt, ro, ru, ta, te, tr, ur). The automated test in src/locales.test.ts validates 100% key parity using validateAddonLocales from @jchat/addon-sdk/i18n-validator:
import { validateAddonLocales } from "@jchat/addon-sdk/i18n-validator"
import { describe, expect, test } from "bun:test"
import * as path from "node:path"
describe("addon locales", () => {
test("maintains key parity across all 20 host languages", () => {
const localesDir = path.join(import.meta.dir, "locales")
const result = validateAddonLocales(localesDir)
if (!result.valid) {
console.error(result.errors.join("\n"))
}
expect(result.valid).toBe(true)
})
})