Custom Extensions
Knitly Custom Extensions
Section titled “Knitly Custom Extensions”Knitly loads an optional custom/ directory so a deployment can add its own server routes and frontend pages without forking. The directory is gitignored, so the public repository ships no built-in extensions — what you put there is yours.
The system has two halves:
- Server:
app.jsdynamically importscustom/server/index.jsand mounts its exportedcustomRouterunder/api/custom. If the file is missing, the import is caught and skipped. - Frontend:
main.tsxandNavigation.tsxuseimport.meta.globoncustom/frontend/index.tsto pull in its exportedcustomRoutesandcustomNavItems.
Directory Layout
Section titled “Directory Layout”custom/├── package.json # workspace member (custom deps live here)├── server/│ └── index.js # must export `customRouter`└── frontend/ └── index.ts # must export `customRoutes` and/or `customNavItems`Server Extensions
Section titled “Server Extensions”custom/server/index.js exports a single Hono router named customRouter. Compose sub-routers onto it; everything mounts under /api/custom.
import { Hono } from "hono";import { ensureSession } from "../../server/src/middleware/auth.js";
export const customRouter = new Hono();
// Reachable at GET /api/custom/statuscustomRouter.get("/status", (c) => { return c.json({ ok: true });});
// Protect a route with the same auth as the core APIcustomRouter.get("/whoami", ensureSession, (c) => { const user = c.get("user"); return c.json({ username: user.username });});Custom routes can import from the server’s lib/ — database helpers, logging, and so on — using relative paths into server/src/.
import { logInfo } from "../../server/src/lib/logging.js";logInfo("Custom route hit.");Frontend Extensions
Section titled “Frontend Extensions”custom/frontend/index.ts exports two optional arrays.
customRoutes:{ path, component }entries registered as top-level routes.customNavItems:{ to, label, icon }entries added to the navigation, whereiconis alucide-preactcomponent.
import { useQuery } from "@tanstack/react-query";import { api } from "../../frontend/src/api/client";
export function Dashboard() { const { data, isLoading } = useQuery({ queryKey: ["custom", "status"], queryFn: () => api.get("/custom/status"), });
if (isLoading) return <p>Loading…</p>;
return ( <div class="p-4"> <h1>Custom Dashboard</h1> <pre>{JSON.stringify(data, null, 2)}</pre> </div> );}import { LayoutDashboard } from "lucide-preact";import { Dashboard } from "./Dashboard";
export const customRoutes = [ { path: "/dashboard", component: Dashboard },];
export const customNavItems = [ { to: "/dashboard", label: "Dashboard", icon: LayoutDashboard },];Follow the same frontend conventions as the core app: server state through TanStack Query, hooks from preact/hooks, no useEffect for data fetching.
Development
Section titled “Development”Custom extensions load automatically in bun run dev. Server routes are available immediately; frontend routes and nav items appear after Vite picks up custom/frontend/index.ts.
bun run devcurl http://localhost:3000/api/custom/statusDeployment
Section titled “Deployment”Because custom/ is gitignored, include it explicitly wherever you deploy.
Docker — copy it into the image before it starts:
FROM knitly-app/knitlyCOPY custom/ /app/custom/Bare metal — copy the directory alongside the rest of the app, then restart the service. If your extension has its own dependencies, run bun install so the custom workspace resolves them.
Security
Section titled “Security”Custom routes run inside the same server, so treat them with the same care as core code:
- Guard anything sensitive with
ensureSession(andrequireRolefor admin-only actions). - Validate and sanitize all input.
- Never commit secrets — read them from environment variables.