New Built for AI agents
One model.
Everything else,
generated.
Define your content once, in TypeScript with Zod. njin generates the REST API, an admin panel and server-rendered pages, then opens it all to AI agents over MCP.
How it works
One model in. Four things out.
Define a model once. njin turns it into a REST API, an admin panel your team can use, server-rendered pages and tools your AI agents can call, each with validation, drafts, languages and access control already wired up.
Read the docsBright lines are what the selected output is built from.
Edit post
Title
Slug
Body
We build calm software for small teams.
Cover
Author
Done. Only the English title changed; the Indonesian copy is untouched.
No controller to write. Validation and unique checks included. Forms, language switch and publish, from the same model. File-based routing. Reads only the live copy. Scoped, revocable, never more than the user can do.
AI agents
Predictable by design.
Easy for agents.
Agents do their best work where there is one obvious way to do things. njin removes the guesswork, so what they build is what you meant.
Convention
Every kind of change has one home, so an agent always knows which file to touch.
Deterministic
One definition feeds the API, the admin and the schema, so they cannot drift apart. Same field, same shape, everywhere.
Features
Then make it
yours.
The model is only the start. Settings, template helpers, events, MCP tools and plugins are plain TypeScript files you own, all registered in one config.ts. No framework source to fork.
- Settings your team can edit
- Helpers and events in plain TypeScript
- Plugins that bundle models, routes and admin pages
- Custom tools your AI agents can call
export default makeVars("general", {
name: "General",
schema: z.object({
title: text({ label: "Title" }, (z) => z.default("njin")),
banner: text({ label: "Banner" }, (z) => z.default("")),
}),
});
// in any template:
// @let(site = await general.get())
// <title>{{ site.title }}</title>
What you get
import { defineHelper } from "@njinlabs/njin";
export default defineHelper("formatDate", (date: string) => {
return new Date(date).toLocaleDateString("en-GB", {
day: "numeric",
month: "short",
year: "numeric",
});
});
// then, in any template:
// {{ formatDate(post.createdAt) }}
What you get
// src/events/order_paid.ts
const orderPaid = makeEvent<{ orderId: string }>();
// src/events/listeners/send_receipt.ts
orderPaid.listen(async ({ orderId }) => {
await sendReceipt(orderId); // errors are logged, never thrown back
});
// when an order is created
afterCreate(order, (record) => {
orderPaid.dispatch({ orderId: record.id });
});
What you get
export default defineTool({
name: "stale_drafts",
description: "List drafts not edited for N days.",
input: { days: z.number().describe("Minimum age in days") },
handler: async ({ days }, { can }) => {
if (!can("post", "read")) throw new Error("Not allowed");
const { data } = await post.read({
filters: { status: "draft" },
});
const cutoff = Date.now() - days * 86_400_000;
return data.filter((p) => +new Date(p.updatedAt) < cutoff);
},
});
// config.ts
// mcp: { tools: [() => import("./src/mcp/stale_drafts")] }
What you get
import { defineConfig } from "@njinlabs/njin/config";
import seo from "my-seo-plugin";
export default defineConfig({
plugins: [seo({ apiKey: process.env.SEO_KEY! })],
});
// A plugin can bundle models, vars, hooks, events,
// routes and its own admin pages. Or scaffold one
// inside your project:
// bunx njin plugin:create analytics
What you get
const app = await createTestApp(config);
it("lets editors read posts", async () => {
const g = await app.createGroup({
name: "Editors",
grants: ["post:*:own"],
});
const user = await app.createUser({ groups: [g.id] });
const res = await app.get("/api/post", { as: user });
expect(res.status).toBe(200);
});
What you get
Get started
Start with one model.
Three commands to a running API, admin panel and website. Open the admin, create your first user, and describe your content.
$ bunx @njinlabs/njin create my-web
$ cd my-web
$ bunx njin dev
# then open localhost:3000/_admin