Shared SDK for ConvStack services. Provides manifest DSL, request handlers, auth helpers, DB utilities, and Vite config.
bun create @convstack/service my-service
cd my-service
cp .env.example .env
bun run service:setup # creates DB, runs migrations, registers with lanyard
bun run dev| File | Purpose |
|---|---|
vite.config.ts |
Calls defineServiceConfig — sets port, plugins, dev-init hook |
src/lib/manifest.ts |
Defines every page, sidebar, and widget the dashboard renders |
src/server/services/self-register.ts |
Exports createSelfRegister(manifest) — called on startup to heartbeat lanyard |
src/routes/api/health.tsx |
Health-check endpoint; lanyard polls this to detect downtime |
Import helpers from @convstack/service-sdk/manifest and add entries to the pages array in your manifest:
import { defineManifest, page, sidebar, item, dataTable } from "@convstack/service-sdk/manifest";
export const MY_MANIFEST = defineManifest({
name: "My Service",
slug: "my-service",
sidebar: sidebar({ items: [item("Foo", "/foo", { icon: "list" })] }),
pages: [
page("/foo", "Foo", { layout: "default" }, [
dataTable("/api/foo", { columns: ["name", "status"] }),
]),
],
});defineManifest validates the manifest at call time (service boot), so schema errors surface immediately.
Create a file route under src/routes/api/. Use createHandler from @convstack/service-sdk/handlers:
// src/routes/api/foo.tsx
import { createHandler } from "@convstack/service-sdk/handlers";
import { requirePermission } from "@convstack/service-sdk/permissions";
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";
import { db } from "~/server/db";
export const Route = createFileRoute("/api/foo")({
server: {
handlers: {
GET: createHandler({
db,
input: z.object({ limit: z.coerce.number().default(50) }),
handler: async (ctx) => {
requirePermission(ctx, "foo:read");
return db.query.foo.findMany({ limit: ctx.input.limit });
},
}),
},
},
});Input is merged from query params, path params, and body (body wins on collision). Validation errors automatically return 422. Unhandled throws return 500.
Declare permissions in the manifest so lanyard can display them in the admin UI:
defineManifest({
// ...
permissions: ["foo:read", "foo:write"],
});Enforce them in handlers with requirePermission:
import { requirePermission } from "@convstack/service-sdk/permissions";
handler: async (ctx) => {
requirePermission(ctx, "foo:write"); // throws 403 if not granted
// ...
}Permissions are assigned to department roles in the lanyard admin panel. ctx.permissions is populated from the proxy headers on every request.
dist/manifest.schema.json — JSON Schema for UIManifest.
dist/wire-contract.openapi.json — OpenAPI spec for the lanyard wire contract.
Regenerate with:
bun run openapi:export