kempo
v4.2.30
Published
A fullstack CMS framework for creating websites with user authentication
Readme
Kempo
A fullstack CMS framework for creating websites with user authentication, group-based permissions, and an admin panel. Built on kempo-server, kempo-css, and kempo-ui.
Quick Start
npx kempo initThis initializes the current directory with everything scaffolded:
| File / Directory | Purpose |
|---|---|
| package.json | Pre-configured with start script |
| .env | Environment variables template |
| .gitignore | Ignores node_modules and .env |
| docker-compose.yml | PostgreSQL database via Docker |
| public/ | Your website's pages, assets, and route handlers |
| public/.config.json | Server routing and middleware config |
| server/db/schema.js | Your app's database table definitions |
| drizzle.config.js | Drizzle ORM config (points to both your schema and kempo's) |
These files are yours to modify. Kempo will not overwrite them on future installs.
Setup the Database
Update the credentials in .env and docker-compose.yml if needed, then:
docker compose up -d
npx drizzle-kit push
node node_modules/kempo/scripts/init-db.jsStart the Server
npm startYour site is running at http://localhost:3000.
Create an Admin User
Register a user through the UI at /register, then promote them:
node node_modules/kempo/scripts/make-admin.jsThis prompts for the user's email and adds them to the system:Administrators group. To remove admin access:
node node_modules/kempo/scripts/remove-admin.jsScaffolded Pages
Kempo scaffolds these pages into your public/ directory. All are fully functional and customizable.
| Route | Page |
|---|---|
| / | Home page |
| /login | Login form |
| /register | Registration form |
| /forgot-password | Password recovery |
| /reset-password/[token] | Password reset (from email link) |
| /verify-email | Email verification notice |
| /verify-email/[token] | Email verification (from email link) |
| /account | User account page (requires login) |
| /admin | Admin dashboard (requires system:admin:access permission) |
| /admin/accounts/users | User management |
| /admin/accounts/users/create | Create user |
| /admin/accounts/users/[id] | Edit user |
| /admin/accounts/groups | Group management |
| /admin/accounts/permissions | Permission management |
| /admin/settings | Settings management |
Project Structure
After setup, your project looks like this:
my-site/
├── .env
├── drizzle.config.js # Drizzle config (your schema + kempo's)
├── package.json
├── public/ # Your website
│ ├── .config.json # Server routing & middleware config
│ ├── index.html # Home page
│ ├── styles.css # Your custom styles
│ ├── login/
│ ├── register/
│ ├── account/
│ ├── admin/
│ └── ...
├── server/
│ └── db/
│ └── schema.js # Your app's database tables
└── node_modules/
└── kempo/
├── dist/kempo/ # SDK, API endpoints, components
├── server/
│ ├── db/schema.js # Framework tables (user, session, group, etc.)
│ └── utils/ # Backend utilities
├── middleware/ # Auth middleware
└── templates/ # Nav and email templatesCustom Database Tables
Define your app's tables in server/db/schema.js. You can reference kempo's tables for foreign keys:
import { pgTable, text, timestamp } from 'drizzle-orm/pg-core';
import { user } from 'kempo/server/db/schema.js';
export const post = pgTable('post', {
id: text('id').primaryKey(),
authorId: text('authorId').references(() => user.id),
title: text('title').notNull(),
content: text('content'),
createdAt: timestamp('createdAt').notNull(),
});Then push to the database:
npx drizzle-kit pushYour drizzle.config.js already points to both schema files, so both framework and app tables are managed together.
Querying the Database
Import the shared database instance from kempo:
import db from 'kempo/server/db/index.js';
import { post } from '../../db/schema.js';
export default async ({ authorId, title, content }) => {
const result = await db.insert(post).values({
id: crypto.randomUUID(),
authorId,
title,
content,
createdAt: new Date(),
}).returning();
return [null, result[0]];
};Framework Database Tables
Kempo provides these tables out of the box:
| Table | Purpose |
|---|---|
| user | User accounts (id, name, email, passwordHash, emailVerified, timestamps) |
| session | Login sessions (token, userId, expiresAt, ipAddress, userAgent) |
| verificationToken | Email verification and password reset tokens |
| group | Permission groups (e.g. system:Users, system:Administrators) |
| userGroup | User-to-group membership |
| permission | Named permissions (e.g. system:admin:access) |
| groupPermission | Group-to-permission assignments |
| setting | Key-value settings with type, visibility, and ownership |
Frontend SDK
Kempo provides a client-side SDK at /kempo/sdk.js for calling API endpoints.
<script type="module">
import { login, getSession, logout } from '/kempo/sdk.js';
const [error, result] = await login({ email: '[email protected]', password: 'secret' });
if(error){
console.error(error.msg);
}
</script>All SDK methods return an [error, data] tuple. On failure, error is { code, msg }. On success, error is null.
Auth
| Method | Parameters |
|---|---|
| register | { email, password, name } |
| login | { email, password } |
| logout | (none) |
| getSession | (none) |
| forgotPassword | { email } |
| resetPassword | { token, password, logoutAll? } |
| changePassword | { currentPassword, newPassword, logoutAll? } |
| verifyEmail | { token } |
Permissions
| Method | Parameters |
|---|---|
| currentUserHasPermission | { permission } |
| currentUserHasAllPermissions | { permissions } |
| currentUserHasSomePermissions | { permissions } |
| getAllCurrentUserPermissions | (none) |
| userHasPermission | { userid, permission } |
| userHasAllPermissions | { userid, permissions } |
| userHasSomePermissions | { userid, permissions } |
| getAllUserPermissions | { userid } |
Users
| Method | Parameters |
|---|---|
| getUsers | { limit?, offset? } |
| getUser | userid |
| createUser | { name, email, password, emailVerified? } |
| updateUser | { id, name?, email?, emailVerified?, createdAt? } |
| deleteUsers | ids (array) |
Sessions
| Method | Parameters |
|---|---|
| getUserSessions | userid |
| deleteUserSession | userid, sessionToken |
| deleteExpiredUserSessions | userid |
Groups
| Method | Parameters |
|---|---|
| listGroups | (none) |
| getUserGroups | userid |
| addUserToGroup | userid, groupName |
| removeUserFromGroup | userid, groupName |
Frontend Components
Kempo provides web components you can use in your pages:
<script type="module" src="/kempo/components/Permission.js"></script>
<kempo-permission has="system:admin:access">
<a href="/admin">Admin Panel</a>
</kempo-permission>| Component | Tag | Purpose |
|---|---|---|
| Permission.js | <kempo-permission> | Conditionally render content based on permissions |
| LogoutBtn.js | <kempo-logout-btn> | Logout button |
| UserName.js | <kempo-user-name> | Display current user's name |
| UserEmail.js | <kempo-user-email> | Display current user's email |
| UserEmailVerified.js | <kempo-user-email-verified> | Display email verification status |
| UserCreatedAt.js | <kempo-user-created-at> | Display account creation date |
Adding Routes
Create files in public/ following the kempo-server routing convention. File names map to HTTP methods:
public/
api/
posts/
GET.js → GET /api/posts
POST.js → POST /api/posts
[id]/
GET.js → GET /api/posts/:id
PATCH.js → PATCH /api/posts/:id
DELETE.js → DELETE /api/posts/:idRoute handlers receive (request, response):
import db from 'kempo/server/db/index.js';
import { post } from '../../../server/db/schema.js';
import { eq } from 'drizzle-orm';
export default async (request, response) => {
const posts = await db.select().from(post);
response.json({ posts });
};request.body is pre-parsed based on Content-Type. Query params are at request.query. Cookies are at request.cookies. Path params from [param] folders are at request.params.
Auth Middleware
The auth middleware protects /account and /admin routes by default:
/account/*— Requires a valid session. Redirects to/loginif not authenticated./admin/*— Requires thesystem:admin:accesspermission. Redirects to/accountif unauthorized.
These are configured in public/.config.json and can be customized.
Default Permissions
The init-db.js script seeds these permissions:
| Permission | Description |
|---|---|
| system:admin:access | Access admin panel |
| system:content:create | Create new content |
| system:content:update | Edit content |
| system:content:delete | Delete content |
| system:user:create | Create new users |
| system:user:read | View users |
| system:user:update | Edit users |
| system:user:delete | Delete users |
| system:group:create | Create new groups |
| system:group:read | View groups |
| system:group:update | Edit groups and manage membership |
| system:group:delete | Delete groups |
| system:permissions:read | View permissions for all users |
| system:permissions:manage | Add or remove permissions |
| system:settings:read | View private settings |
| system:settings:update | Update settings |
| system:custom-settings:manage | Create and delete custom settings |
Two default groups are created:
- system:Users — Assigned to all users, no permissions by default.
- system:Administrators — Full system access (all permissions above).
Default Settings
| Setting | Default | Type | Public | Description |
|---|---|---|---|---|
| site_name | Kempo Site | string | yes | The name of the website |
| session_duration_days | 7 | number | no | Session duration in days |
| allow_registration | true | boolean | yes | Allow new users to register |
| require_email_verification | false | boolean | no | Require email verification before account access |
| verification_url | http://localhost:3000/verify-email/{{token}} | string | no | Email verification URL template |
| password_reset_url | http://localhost:3000/reset-password/{{token}} | string | no | Password reset URL template |
Settings can be managed through the admin panel at /admin/settings or the SDK.
Kempo uses Resend for sending emails. Set RESEND_API_KEY and EMAIL_FROM in your .env file.
Built-in email templates (in templates/emails/):
verify-email.html— Email verification linkreset-password.html— Password reset link
Styling
Kempo uses kempo-css for styling and kempo-ui for UI components. Both are served automatically via custom routes configured in public/.config.json.
<link rel="stylesheet" href="/kempo-css/kempo.min.css">
<script type="module" src="/kempo-ui/components/Import.js"></script>Refer to the kempo-css docs and kempo-ui docs for available utility classes and components.
The Rich Text Editor
The admin's WYSIWYG is kempo-ui's <k-html-editor>, which is built on Lexical. Kempo ships Lexical itself rather than letting the editor fetch it from a CDN, so the editor keeps working offline, behind a strict Content-Security-Policy, on an air-gapped install, and during a CDN outage — and no third party sees a request every time an administrator opens a page.
Nothing is required to set this up. npm run build bundles Lexical into dist/kempo/vendor/lexical/, kempo's middleware serves it, and window.kempo.lexicalUrl points the editor at it:
window.kempo = { ..., lexicalUrl: '/kempo/vendor/lexical' };To use a CDN instead — or a copy you host elsewhere — change that value, or set lexical-src on an individual editor. Removing it falls back to https://esm.sh.
Lexical cannot be served straight from node_modules: its published entry reads process.env.NODE_ENV, which does not exist in a browser, and its builds import bare specifiers that a browser cannot resolve. The build therefore bundles each package with esbuild, with code splitting so all nine share a single copy of Lexical's core — Lexical relies on class identity, and duplicate copies produce an editor that loads but stays empty.
Development
Running the framework
Kempo is developed against kempo-demo, a real consumer project that installs kempo, kempo-blog and the rest the way a user would. Running a server inside this repo instead was misleading: paths resolve differently when the framework runs from within node_modules/kempo than when it runs from its own directory, so problems appeared here that users never hit, and vice versa.
cd ../kempo-demo
npm install
npm run link:local # symlink the sibling checkouts into node_modules
npm start # http://localhost:9876After editing src/ here, rebuild so the demo picks it up — dist/ is what actually gets served:
npm run build:deps # from kempo-demo; builds kempo, kempo-server, kempo-ui, kempo-cssTo edit the admin portal without rebuilding on every change, point the middleware at the source instead of the build by adding adminRoot to the demo's public/.config.json:
{
"middleware": {
"custom": ["../node_modules/kempo/middleware/kempo.js"],
"adminRoot": "node_modules/kempo/src/admin"
}
}Running the tests
Most of the suite needs nothing, but the extension-lifecycle and HTTP-contract tests drive a real database and skip themselves when none is reachable. To run everything:
docker compose up -d # postgres on 5434, separate from kempo-demo's on 5433
npx drizzle-kit push --force # apply the schema
npm test.env.example has the matching DATABASE_URL. The suite seeds its own groups and permissions, and cleans up the rows it creates.
Building for Distribution
When preparing the framework for npm release, minify and optimize the source code:
npm run buildThis processes all files in src/ and outputs to dist/:
- JavaScript files are minified with terser, mangled, and source maps are generated
- HTML files are minified while preserving CMS front matter comments (important for page templating)
- JSON files are minified
- SVG icons and other assets are copied as-is
The build achieves ~15% size reduction and runs in ~0.6s.
Output Structure:
dist/
├── admin/ # Minified admin portal
├── kempo/ # Minified framework SDK, API routes, components
└── ...See spec/concepts/build.md for detailed build documentation.
Project Structure
Framework source:
kempo/
├── src/
│ ├── admin/ # Admin portal source (minified to dist/admin/)
│ └── kempo/ # Framework SDK & API source (minified to dist/kempo/)
├── server/ # Backend utilities (not minified)
├── middleware/ # Auth middleware
├── app-public/ # Consumer boilerplate (not minified)
├── app-templates/ # Email and page templates (not minified)
├── app-server/ # Server scaffolding (not minified)
└── dist/ # Built & minified output (generated by `npm run build`)Consumer project (created by npx kempo init):
my-site/
├── public/ # Copied from kempo's app-public/
├── server/db/schema.js # Copied from kempo's app-server/
├── templates/ # Copied from kempo's app-templates/
└── ...License
MIT
