npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

kilo-cms

v0.3.1

Published

An installable, config-driven CMS engine for Next.js — admin panel, RBAC, a generic field-type engine, and an API, mounted into your own app. Define your content schema via config, not a hand-maintained registry.

Readme

Kilo CMS

A schema-driven, config-driven CMS engine, built specifically for Next.js. Instead of generating code or hand-editing a central registry, you describe your content model as plain config — fields, relations, validation, list views — and Kilo CMS turns that into a working admin panel, RBAC, and API, mounted directly inside your own app's routes.

Status: pre-1.0, proven end-to-end against one real production app (kilostudio.id's apps/site), not yet used by a second, independent project. API surface may still change.

Looking for collaborators. The repo is currently private; reach out if you want in as a collaborator.

What you get

  • A generic field-type engine (text, richtext, image, relation, array, blocks, and more) driving CRUD, validation, list rendering, and filtering from a plain config object — no schema-generation step, no code-gen.
  • Role-based access control, per content type and per field.
  • Media library, review/approval workflow, scheduled publishing, localization, webhooks, an AI writing assistant, and a dashboard builder — all included, all config-driven.
  • Auth via better-auth, mounted the same one-line way better-auth itself mounts into a Next.js route (export const { GET, POST } = toNextJsHandler(auth)).
  • A CLI (npx kilo-cms) for scaffolding new collections without hand-editing a central registry.

Install

npm install kilo-cms

next, react, and react-dom are peer dependencies — Kilo CMS uses whatever versions your own project already has.

Add transpilePackages: ['kilo-cms'] to your next.config.ts — Kilo CMS ships raw TypeScript source (including its 'use client' admin components), not a prebuilt dist, so Next's own compiler needs to process it directly.

Quickstart

Define a collection — split into a fields file (client-safe, no database import) and a table file (server-only, the actual drizzle table):

// src/collections/projects/fields.ts
import { defineCollectionFields } from 'kilo-cms/collections'

export const projectsFields = defineCollectionFields({
  kind: 'collection',
  slug: 'projects',
  label: 'Projects',
  labelSingular: 'Project',
  titleField: 'title',
  defaultSort: { field: 'sortOrder', dir: 'asc' },
  groups: [{ id: 'overview', label: 'Overview' }],
  fields: [
    { key: 'title', type: 'text', label: 'Title', group: 'overview', required: true, listColumn: true, listPrimary: true },
  ],
})
// src/collections/projects/table.ts
import 'server-only'
import { pgTable, text } from 'drizzle-orm/pg-core'
import { timestamps } from 'kilo-cms/schema'
import { defineCollectionTable } from 'kilo-cms/collections/table'

export const projects = defineCollectionTable('projects', pgTable('projects', {
  id: text('id').primaryKey(),
  title: text('title').notNull(),
  ...timestamps,
}))

Wire it up — one config file, and a place that creates your database connection (both plain data/setup, no magic):

// kilo.config.ts — the one file you edit to add/list collections
import { projectsFields } from './src/collections/projects/fields'
import './src/collections/projects/table' // side-effecting: registers the table

export const collections = [projectsFields]
export const locales = { codes: ['en'], default: 'en', labels: { en: 'English' } }
export const currency = { locale: 'en-US', currency: 'USD' }
// src/lib/db.ts — creates your db connection AND registers Kilo CMS with it
import postgres from 'postgres'
import { drizzle } from 'drizzle-orm/postgres-js'
import { defineKiloConfig } from 'kilo-cms/config'
import { createKiloAuth } from 'kilo-cms/auth'
import { schema } from './schema' // your own merged schema — see below
import { collections, currency, locales } from '../../kilo.config'

const client = postgres(process.env.DATABASE_URL!)
export const db = drizzle({ client, schema })

defineKiloConfig({ db, collections, locales, currency })

export const auth = createKiloAuth({
  db,
  secret: process.env.BETTER_AUTH_SECRET!,
  baseURL: process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000',
})
// src/lib/schema.ts — merges Kilo CMS's own tables (auth, RBAC, media, settings, ...) with yours
export * from 'kilo-cms/schema'
export * from '../collections/projects/table'

import { mergeSchema } from 'kilo-cms/schema'
import { projects } from '../collections/projects/table'
export const schema = mergeSchema({ projects })

mergeSchema({ ...yourTables }) is just { ...kiloSchema, ...yourTables } — it exists so adding a content table only touches the object you pass in, never a second copy of kiloSchema hand-spread somewhere else. The export * from '../collections/projects/table' line above it still has to stay, though: drizzle-kit finds tables by scanning a schema file's top-level exports, not by evaluating a merged object at its own config-load time. If you'd rather not maintain that re-export line per collection either, point drizzle-kit's own schema option at a glob instead — it accepts an array of paths/globs natively:

// drizzle.config.ts
export default defineConfig({
  schema: ['./src/lib/schema.ts', './src/collections/*/table.ts'],
  // ...
})

That way src/lib/schema.ts only needs to exist for export * from 'kilo-cms/schema' and the mergeSchema(...) runtime object — every host content table is picked up by the glob automatically the moment its table.ts exists, with no per-collection edit to this file at all.

Registration: instrumentation.ts

Call defineKiloConfig() once, as early as possible, from Next.js's instrumentation.ts register() hook — it runs once per server process, before any request is handled:

// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    await import('@/lib/db')
  }
}

This is enough for every dynamic route (every admin page/route, since none of them are statically generated) — the process instrumentation ran in is the same one that later serves those requests. It is not enough on its own for anything reachable from generateStaticParams on your public-facing pages: Next's build-time static generation spawns separate worker processes, and instrumentation.ts isn't guaranteed to have run in all of them before a page module evaluates. For those pages, importing @/lib/db (directly, or transitively through whatever data-fetching module they already import) is what actually guarantees registration in that worker — instrumentation.ts is a convenience for the admin side, not a replacement for that.

Middleware

Kilo CMS ships an Edge-safe, cookie-presence-only pre-filter that redirects unauthenticated visitors away from /admin/* before a page even renders — a fast filter, not the real security boundary (every admin page/route still validates the session and RBAC permissions for real). Your own middleware.ts re-exports it and defines matcher itself (Next.js statically analyzes matcher in the file it scans — it can't be re-exported through another module):

// middleware.ts
export { kiloAdminMiddleware as middleware } from 'kilo-cms/middleware'

export const config = {
  matcher: ['/admin/:path*'],
}

Admin pages use requireAdminSession() from kilo-cms/admin/require-session instead of hand-rolling getSessionWithPermissions() + redirect():

import { requireAdminSession } from 'kilo-cms/admin/require-session'

const result = await requireAdminSession((r) => r.can('projects', 'read'), '/admin/dashboard')

CLI

Run from your own app's directory:

npx kilo-cms add-collection <slug> [--fields "..."]  # scaffold a new collection's fields.ts + table.ts
npx kilo-cms sync                                    # regenerate .kilo/types.gen.ts from src/collections/*
npx kilo-cms migrate                                 # apply Kilo CMS's own package-owned migrations
npx kilo-cms init                                    # fill in missing .env secrets, migrate, create the first admin user

add-collection scaffolds the two files and prints the exact lines to paste into kilo.config.ts — deliberately not an auto-codemod (a script that edits your config file wrong is worse than a 10-second copy-paste). Without --fields, you get the same title/slug/sortOrder starter as before. With --fields, it builds exactly the fields you list instead — a plain-text shorthand for the common case, not a substitute for hand-editing fields.ts afterward for anything it doesn't cover:

npx kilo-cms add-collection testimonials \
  --fields "quote:textarea(required,rows=4),author:text(required,listPrimary,maxLength=80),rating:rating(max=5),featured:boolean,logo:image"

Each entry is key:type or key:type(opt,opt2=value). Supported types: text, textarea, richtext, number, boolean, select, multiselect, tags, image, file, date, datetime, color, rating. Options: required, nullable, listColumn, listPrimary, searchable, sortable, filterable, readOnly, maxLength=N, rows=N, format=slug|email|url|password|tel, options=a|b|c (select/multiselect). Relations, joins, arrays, blocks, groups, and JSON fields aren't part of this shorthand — add those by hand, same as you'd tune anything else the generator scaffolds.

migrate applies Kilo CMS's own schema (auth, RBAC, media, settings, dev-tools, admin-ui, locales — see src/schema/) against your DATABASE_URL, tracked in its own kilo_cms_migrations table, independent of your own app's migration history for its content tables.

init fills in BETTER_AUTH_SECRET and CMS_SETUP_TOKEN in your .env if either is missing (generated, never overwriting a value that's already there), runs migrate, then creates your first admin user via the existing /api/setup/admin route — it needs your app already running (npm run dev in another terminal) since that route executes inside your app's own Next.js runtime:

npx kilo-cms init --name "Your Name" --email [email protected] --password "at least 12 characters" [--url http://localhost:3000]

The only variable you're still expected to set yourself is DATABASE_URL. If init had to generate a fresh CMS_SETUP_TOKEN, it stops after migrate and asks you to restart your dev server first — the already-running process only read .env once, at its own startup, so it can't see a token that didn't exist yet. Run the same init command again after restarting.

Package layout

| Export | What it is | |---|---| | kilo-cms/config | defineKiloConfig() — the one call that wires db + collections + locale/currency together | | kilo-cms/collections | Client-safe: field types, defineCollectionFields(), the registry, value helpers | | kilo-cms/collections/table | Server-only: defineCollectionTable() | | kilo-cms/collections/server | Server-only: validation, workflow, filters, the query engine | | kilo-cms/schema | Kilo CMS's own drizzle tables (auth, RBAC, media, settings, dev-tools, admin-ui, locales) + mergeSchema() | | kilo-cms/auth, kilo-cms/auth-client | createKiloAuth() factory + the better-auth React client | | kilo-cms/admin/* | Admin UI pages and shared components | | kilo-cms/admin/require-session | requireAdminSession() — the shared session/RBAC guard for admin pages | | kilo-cms/middleware | kiloAdminMiddleware — Edge-safe cookie-presence pre-filter for /admin/* | | kilo-cms/routes/* | Route Handler logic for the admin API, public API helpers, auth, cron |

Status / roadmap

Built by extracting a working, in-production CMS out of the app it was originally embedded in — see kilostudio.id's WORKLOG.md for the full history of that extraction. Not yet done:

  • A single catch-all admin route instead of one thin file per page (deliberately skipped for now — today's thin per-page files are already minimal; a hand-rolled catch-all router would trade that for real complexity with no proven need yet).
  • A second, independent consumer project to prove the install story beyond the one app this was extracted from.

Versioning

Semantic versioning, while the major version stays 0: a minor bump (0.x.0) means new or changed public API surface (a new export, a new CLI command); a patch bump (0.x.y) means a bug fix with no public API change. Every release is documented in CHANGELOG.md and tagged in git (v0.1.0, v0.1.1, v0.2.0, ...) — check the changelog before upgrading a consumer's dependency range, especially across a minor bump.

License

Proprietary — all rights reserved. This is not an open-source project: the repo is private, and access is by invitation as a collaborator, not by public license. Contact the author before using this outside of an explicitly authorized project.