A schema-driven headless CMS derived from your Drizzle ORM definitions. Define your database schema once โ get a type-safe admin interface for free.
- Single source of truth: Your Drizzle schema defines database tables, TypeScript types, validation rules, AND CMS fields
- Minimal dependencies: Core stack is
drizzle-orm+zod+drizzle-zod+@std/media-types(all zero transitive deps) - Secure by default: CSRF protection, JWT auth (
HttpOnly+SameSite=Laxcookies, configurable โ see the Security Guide), row-level policies, column-level policies, XSS-safe templates - Flexible & extensible: Pluggable auth, custom validation, row & column policies, plugins with Worker isolation
- Cross-runtime: Works in Deno and Node.js โ Web Standard
Request/Responseeverywhere - Database-agnostic: Works with any Drizzle-supported database (Postgres, MySQL, SQLite)
# Deno
deno add jsr:@hotsauce/core jsr:@hotsauce/ui jsr:@hotsauce/cms
# Node
npx jsr add @hotsauce/core @hotsauce/ui @hotsauce/cmsimport { createCmsHandler } from '@hotsauce/cms';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema.ts';
const client = postgres(Deno.env.get('DATABASE_URL')!);
const db = drizzle(client, { schema });
const handler = createCmsHandler({
schema,
db,
basePath: '/admin',
auth: 'dangerously-open', // No authentication (dev mode)
});
// Use with any server
Deno.serve(handler);Each package has its own README with detailed API documentation:
| Package | Purpose | Docs |
|---|---|---|
@hotsauce/core |
Schema introspection, field mapping, validation | README |
@hotsauce/ui |
HTML generation, form rendering, views | README |
@hotsauce/cms |
CRUD route handlers (Request โ Response) | README |
@hotsauce/auth |
JWT, password hashing, TOTP, account management | README |
@hotsauce/workers |
Plugin runtime with Worker isolation | README |
@hotsauce/plugins |
Official plugins (audit-log, puck, s3-storage) | README |
packages/
โโโ core/ # Schema introspection, field mapping, validation
โ โ # Runtime-agnostic, zero Deno/Node specific code
โ โโโ schema/ # Schema parsing and metadata extraction
โ โโโ fields/ # Column type โ CMS field mapping
โ โโโ extend/ # $cms() column/table metadata
โ โโโ validation/ # drizzle-zod integration
โ
โโโ ui/ # HTML generation, form rendering
โ โ # Pure functions returning strings, zero dependencies
โ โโโ html.ts # Tagged template with auto-escaping
โ โโโ forms/ # Form field renderers (text, select, etc.)
โ โโโ views/ # List, detail, edit views
โ โโโ components/ # Layout, pagination, alerts
โ
โโโ cms/ # CRUD route handlers (Web Standard Request/Response)
โ โ # Bring Your Own Server - works with any framework
โ โโโ router.ts # URL routing and handler dispatch
โ โโโ crud.ts # List, create, read, update, delete handlers
โ โโโ plugins/ # Plugin registry and service (uses workers)
โ โโโ policies/ # Row and column-level security
โ โโโ tokens/ # CSRF, source token utilities
โ
โโโ auth/ # Authentication and authorization
โ โ # JWT, password hashing, TOTP 2FA, account management
โ โโโ jwt.ts # JWT sign/verify (HMAC-SHA256)
โ โโโ password.ts # PBKDF2-SHA256 password hashing
โ โโโ totp.ts # RFC 6238 TOTP utilities
โ โโโ account/ # Self-service account management
โ
โโโ workers/ # Plugin runtime with Worker isolation
โ โ # Compatible with Deno and Node.js 20+
โ โโโ executor.ts # Manages Worker instances and in-process plugins
โ โโโ guard.ts # Worker context detection
โ โโโ validate.ts # Serialization validation
โ
โโโ plugins/ # Official plugins
โโโ audit-log/ # Logs all CRUD operations
โโโ puck/ # Puck visual editor integration
โโโ s3-storage/ # S3/R2/MinIO file storage
The handlers package exports a single function that returns a Web Standard Request โ Response handler. Wire it up to any server:
import { createCmsHandler } from '@hotsauce/cms';
import * as schema from './schema.ts';
const handler = createCmsHandler({
schema,
db,
basePath: '/admin',
auth: 'dangerously-open', // Or { provider: ..., policies: ... } or { external: ... }
});
// Deno
Deno.serve(handler);
// Node 20+
import { createServer } from 'node:http';
// ... convert Request/Response
// Hono
app.all('/admin/*', (c) => handler(c.req.raw));
// Express
app.use('/admin', expressAdapter(handler));โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Drizzle Schema + CMS Hints โ
โ โ
โ const posts = pgTable('posts', { โ
โ title: varchar('title').notNull(), โ
โ body: text('body'), โ
โ }); โ
โโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโผโโโโโโโโโโโโโโ
โผ โผ โผ
โโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ
โ DB โ โ Zod โ โ CMS โ
โ Tables โ โ Schemas โ โ Fields โ
โโโโโโโโโโโ โโโโโโโโโโโโ โโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโ
โ Admin UI โ
โ (auto-gen) โ
โโโโโโโโโโโโโโโโโโ
| Drizzle Type | CMS Field | Notes |
|---|---|---|
varchar |
text |
With maxLength from column def |
text |
textarea |
Override to richtext via hint |
boolean |
checkbox |
|
integer / real |
number |
|
timestamp |
datetime |
|
date |
date |
|
json / jsonb |
object |
Requires sub-schema hint |
text[] / arrays |
list |
Postgres-only |
pgEnum |
select |
Postgres-only |
| Foreign key | relation |
Auto-detected from references |
json/jsonb + $cms({ file: true }) |
file |
Multipart upload + file serving route |
Foreign key columns are automatically detected and rendered as select dropdowns:
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: varchar('title', { length: 200 }).notNull(),
authorId: integer('author_id').references(() => users.id), // โ Select dropdown
});The CMS will:
- Show a dropdown with all users on the edit form
- Display the related record's name (e.g., "Alice Johnson") in list and detail views
- Format as "ID (Name)" for clarity (e.g., "1 (Alice Johnson)")
Junction tables are automatically detected and rendered as checkbox lists:
export const postCategories = pgTable('post_categories', {
postId: integer('post_id').notNull().references(() => posts.id),
categoryId: integer('category_id').notNull().references(() => categories.id),
}, (table) => [
primaryKey({ columns: [table.postId, table.categoryId] }),
]);The CMS will:
- Detect junction tables (2 FKs to different tables)
- Hide junction tables from navigation
- Show checkbox list on the edit form for related records
- Display comma-separated values in list and detail views (e.g., "Technology, Design")
By default, the CMS auto-generates validation schemas from your Drizzle tables using drizzle-zod. For custom validation (email formats, password strength, etc.), pass your own parsers:
import { z } from 'zod';
import { createInsertSchema, createUpdateSchema } from 'drizzle-zod';
import type { Parsers } from '@hotsauce/cms';
// Extend drizzle-zod schemas with custom rules
const usersInsertSchema = createInsertSchema(users, {
email: z.string().email(), // Add email format validation
});
const usersUpdateSchema = createUpdateSchema(users, {
email: z.string().email().optional(),
});
// Parsers are validation-library agnostic
// Any function that takes unknown data and returns parsed data (or throws) works
const parsers: Parsers = {
users: {
insert: (data) => usersInsertSchema.parse(data),
update: (data) => usersUpdateSchema.parse(data),
},
};
const handler = createCmsHandler({
db,
schema,
auth: 'dangerously-open',
parsers, // Tables without custom parsers use auto-generated schemas
});The parser interface is simple โ any validation library works:
interface TableParsers {
insert?: (data: unknown) => unknown; // For create operations
update?: (data: unknown) => unknown; // For edit operations
}| Option | Purpose |
|---|---|
auth |
Authentication: 'dangerously-open' or { provider, ... } (JWT) |
policies |
Row-level security with SQL conditions + column-level read/write control |
parsers |
Custom validation (Zod, Valibot, Arktype, or any library) |
onError |
Error logging integration (Sentry, Datadog, etc.) |
- Schema introspection
- Field type mapping (column โ CMS field)
- Zod validation via drizzle-zod (auto-generated)
- Custom validation parsers (library-agnostic)
- Server-rendered HTML forms (zero JS dependencies)
- XSS-safe template literals with auto-escaping
- List, detail, and edit views
- HTML5 native form validation (required, maxlength, pattern)
- Auto-generated CRUD routes (BYOS: Bring Your Own Server)
- Web Standard Request/Response handlers
- Relation field pickers (FK โ select dropdown with display labels)
- Many-to-many relationships (junction table detection, checkbox UI)
- Column metadata hints via
$cms()(file, hidden, readOnly) - Table metadata hints via
$cms()(frontendUrl, hidden) - JWT authentication (cookie-based tokens)
- Two-factor authentication (TOTP)
- External authentication (reverse proxy / OAuth integration)
- Row-level security policies (atomic authorization)
- Column-level access control (read/write policies per field)
- Multi-tenant compatibility (shared database with tenant column)
- Plugin system with Worker isolation (Deno + Node.js 20+)
- File uploads (base64 in DB, validation, serving route)
- Publish alpha release to jsr (Deno support)
- File uploads (S3/R2 cloud storage adapter)
- Media library UI (browse, reuse previously uploaded files)
- Project: Project website
- Project: Security disclosure policy
- API: Shared policy API (reuse CMS row/column policies in your app routes)
- API: Expose plugin utilities for frontend use (e.g., S3 signed download URLs)
- S3: orphan garbage collection (cleanup objects not referenced by any record)
- CDN: add native support (public + private files, cache invalidation)
- Plugins: Seamless UI (in-page S3 uploads and block editing without navigation)
- Plugins: Additional worker config - timeout, worker response validation, load testing
- Plugins: Permission/compatibilities approval (env var hash or DB table gate for CSP/capability changes)
- Plugins: Data obfuscation (PII/credential redaction)
- Plugins: Audit logging
- UI: Search for list, grid and picker views (full-text search, filters, pagination)
- UI: Customizable UI components
- Auth: 2FA backup codes (recovery codes for lost authenticator)
- Tests: Add tests for NodeJS runtime
- Tests: Regression tests on demo app, e.g. catch CSP violations.
Schema hints example:
import '@hotsauce/core/extend';
import { jsonb, pgTable } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
avatar: jsonb('avatar').$cms({ file: true }),
});| Database | Status | Notes |
|---|---|---|
| PostgreSQL | โ Primary | Full feature support including arrays, enums |
| SQLite | โ Tested | Lightweight/edge deployments, text enums |
| MySQL | ๐ฎ Planned | Core features work, needs integration tests |
The core schema introspection is database-agnostic via Drizzle's abstractions. Database-specific features (arrays, native enums) degrade gracefully on other databases.
Server-rendered doesn't mean slow. A full admin list page โ routing, query, policy checks, HTML render โ completes in ~290 ยตs end to end (~3,400 req/s single-threaded on in-memory SQLite; Apple M1 Pro). Route parsing is ~180 ns and full schema introspection ~15 ยตs, so cold starts stay light.
Run deno task bench to reproduce, and see BENCHMARKS.md for methodology, the full suite, and the live trend chart. Every PR is benchmarked against its base commit to catch regressions.
This project is developed with Deno โ no Node.js or npm required locally.
Node.js and npm are used only to validate the generated npm packages:
- CI: runs Node.js compatibility tests for the npm build output
- Local (optional):
deno task test:npmruns the same Node.js E2E tests in Docker
# Clone
git clone https://github.com/hotsauce-team/hotsauce
cd hotsauce
# Enable pre-commit hooks (runs fmt/lint/check)
git config core.hooksPath .githooks && \
chmod +x .githooks/pre-commit
# Run tests
deno task test
# Type check
deno check packages/*/mod.ts
# Format
deno fmt
# Lint
deno lint
# Run all checks manually
deno task hooks:run
# Build npm packages (for publishing)
deno task build:npmNode.js compatibility is tested in CI and achieved via JSR + dnt for npm publishing.
| Layer | Package | Transitive Deps |
|---|---|---|
| ORM | drizzle-orm |
0 |
| Validation | zod |
0 |
| SchemaโZod | drizzle-zod |
0 |
| Database Driver | User's choice | Varies |
All direct dependencies have zero transitive dependencies. You bring your own database driver (postgres, better-sqlite3, mysql2, etc.).
MIT