Tooty CMS is a multi-tenant publishing platform built on Next.js + Drizzle with a governed extension model.
Tooty CMS includes historical lineage from the original Vercel Platforms Starter Kit. Retain original license and attribution notices where applicable.
It supports:
- Multi-site routing by domain/subdomain
- Theme-based frontend rendering (Nunjucks + theme assets)
- Plugin-based behavioral extensions (kernel hooks + core APIs)
- Taxonomy + data domains (post-type style content lanes)
- Media library with hashed uploads and derived image variants
Core is authoritative for:
- Routing, auth, and security
- Database schema and writes
- Extension loading and render pipeline
- Side effects via core APIs
Extensions are governed:
- Plugins can extend behavior through typed contracts and capability flags
- Themes can extend presentation but cannot perform side effects
See:
docs/EXTENSION_CONTRACTS.mddocs/THEME_SANDBOX_CONTRACT.mddocs/ARCHITECTURE.md
- Next.js (App Router)
- TypeScript
- Drizzle ORM + Postgres
- NextAuth
- Nunjucks (theme template rendering)
- Playwright + Vitest
app/— routes, API handlers, dashboard UIlib/— core domain logic (actions, runtime, themes/plugins/kernel)themes/— optional local theme folder (additional theme roots can be configured viaTHEMES_PATH)plugins/— optional local plugin folder (additional plugin roots can be configured viaPLUGINS_PATH)docs/— architecture + contracts + subsystem docstests/— unit and integration tests
Themes live in themes/<theme-id>/ by default.
You can override source roots with THEMES_PATH (comma-separated absolute or workspace-relative paths).
Required:
theme.json
Common files:
templates/home.htmltemplates/index.htmltemplates/header.html(shared partial)templates/footer.html(shared partial)assets/style.cssassets/theme.jspublic/...(served via/theme-assets/<theme-id>/...)
Taxonomy template hierarchy supports files like:
tax_category_<slug>.htmlcategory-<slug>.htmlcategory.htmlarchive.htmlindex.html
Example: themes/tooty-light/templates/tax_category_documentation.html is used for /c/documentation.
Plugins live in plugins/<plugin-id>/ by default.
You can override source roots with PLUGINS_PATH (comma-separated absolute or workspace-relative paths).
Required:
plugin.json
Optional:
index.mjsexportingregister(kernel, api)
Runtime capability flags (manifest capabilities) are enforced:
hooksadminExtensionscontentTypesserverHandlersauthExtensions(experimental)
If a plugin uses undeclared capabilities, core throws [plugin-guard] errors.
Runtime baseline:
- Node.js
22LTS
- Install system prerequisites
Required for local development + testing:
git- Node.js
22 pnpm
Required only if you want the full local browser matrix:
microsoft-edge(optional unless you want Edge included alongside Chromium, Firefox, and WebKit)
Optional for local branded domain routing:
caddydnsmasq
Optional for CLI maintenance / AI-agent-assisted ops:
libpq(psqlclient for direct Postgres/Neon inspection)neonctl(dedicated Neon CLI on macOS)vercel(CLI for environment sync, deploy inspection, and platform actions)
macOS (Homebrew):
brew install git node@22 pnpm caddy dnsmasq libpq neonctl
brew install --cask microsoft-edge
npm install -g vercelNotes:
gitis required for hooks, sync, and normal contributor workflow.- Node.js
22+pnpmare the required runtime/package-manager baseline for dev, tests, and CI parity. libpqprovides the localpsqlclient for direct Postgres/Neon checks.neonctlis optional, but useful if you want a dedicated Neon CLI on macOS.vercelis installed vianpm, not Homebrew, in this setup.microsoft-edgeis optional, but required if you want the full Playwright browser matrix (chromium,firefox,webkit,edge) to include Edge on macOS.- If you use Homebrew
node@22, ensure it is first on your shellPATH.
Debian / Ubuntu:
sudo apt-get update
sudo apt-get install -y curl ca-certificates git caddy dnsmasq postgresql-client
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
sudo corepack enable
npm install -g vercelNotes:
pnpmis provided through Corepack after Node.js22is installed.git+ Node.js22+pnpmare the required baseline for dev, tests, and CI parity.postgresql-clientprovides the localpsqlclient for Postgres/Neon.- Tooty does not require a dedicated Neon CLI for local development; standard Postgres tooling is enough. If you want one, install it separately for your distro.
vercelis installed vianpm, notapt, in this setup.- If you prefer a different Node version manager (
nvm,volta), keep the runtime at Node.js22.
Optional Playwright browser install (recommended for local cross-browser validation):
npx playwright install --with-deps chromium firefox webkit- Install dependencies
npm install- Configure environment
cp .env.example .envRecommended for isolation:
- Set
POSTGRES_TEST_URLto a separate Neon branch/database for integration and e2e tests. - If
POSTGRES_TEST_URLis empty, test scripts fall back toPOSTGRES_URL. - Full guide:
docs/TESTING_DB.md
- Run database schema push (used in build script) and start dev server
npm run devOptional but recommended:
npm run hooks:installApp runs at:
http://localhost:3000
If you want clean local domains and multiple local sites at once, the recommended macOS setup is:
dnsmasqfor local.testname resolutionCaddyfor hostname-based reverse proxying to per-project ports
Why:
/etc/hostsdoes not support wildcard domains.testis a reserved dev-safe TLD, but it does not remove the need for local DNS/proxy routing- a reverse proxy lets you keep multiple apps running at once without browser port suffixes
Recommended pattern:
- each local app runs on its own high port
localhost:<port>still works directlyCaddylistens on port80and routes by hostname
Example:
robertbetan.testandapp.robertbetan.test->127.0.0.1:3000fernain.testandapp.fernain.test->127.0.0.1:3001
Example Caddyfile:
robertbetan.test, app.robertbetan.test {
reverse_proxy 127.0.0.1:3000
}
fernain.test, app.fernain.test {
reverse_proxy 127.0.0.1:3001
}Example dnsmasq rules (if wildcard subdomains are desired):
address=/robertbetan.test/127.0.0.1
address=/fernain.test/127.0.0.1
This lets you use all of these at the same time:
http://localhost:3000http://robertbetan.testhttp://fernain.test
Tooty env pairing for a branded local install:
NEXTAUTH_URL=http://robertbetan.testNEXT_PUBLIC_ROOT_DOMAIN=robertbetan.testADMIN_PATH=cp
If you are not using a proxy, keep the explicit dev port in those URLs.
- Keep the project framework set to
Next.js. - This repo includes
vercel.json("framework": "nextjs") to prevent accidental fallback toOther. - If Vercel builds this app as
Other, deploys can look "ready" but serve a platform404 NOT_FOUNDfor all domains. - Keep runtime aligned with Node.js
22(engines.node). - Set
DEBUG_MODE=falsein production. - For app subdomain routing, ensure
app.<root-domain>has an explicit DNS record pointing to the Vercel target shown in your domain settings.
Tooty is currently in pre-1.0.0 unstable development.
- Version format in this phase is
0.MINOR.PATCH. - In this phase,
MINORis treated as the unstable major line (0.2.x,0.3.x, etc.). - Breaking changes may occur in any
0.xrelease. - We explicitly reserve the right to make breaking changes until the project reaches
1.x.x. - After
1.x.x, standard SemVer expectations apply (breaking changes only on major bumps).
First-run setup is available at /setup until setup is completed.
Current setup flow:
- Save environment values (local
.env, Vercel env API, or lambda backend based on runtime/backend setting) - Initialize schema (auto-check existing tables and only run init when required)
- Persist setup completion and bootstrap admin metadata
- First admin user is created on first OAuth login (no pre-seeded auth user row)
Notes:
site_urlis used as canonical root URL in dashboard/theme contexts.- In local mode, missing port is normalized from
NEXTAUTH_URL/PORTto avoid broken links.
AI completion is optional.
- Without
OPENAI_API_KEY, AI completion endpoints are disabled and return a clear non-configured response. - Core editor functionality (writing, formatting, media, taxonomy, publishing) still works without any AI key.
- You can optionally set
OPENAI_MODEL(default:gpt-4o-mini) when enabling AI.
Unit tests:
npm run testIntegration tests (Playwright):
npm run test:integrationFull suite:
npm run test:allFor non-trivial core work, use this checkpoint discipline:
- Implement the change in
tooty-cms. - Run the required green gates:
npm run testnpm run test:integration
- If both are green, create a local checkpoint commit before starting the next substantial work chunk.
Commit guidance:
- Commit locally only unless you explicitly intend to push.
- Use a conventional commit message.
- A WIP checkpoint is acceptable when the code is validated, for example:
chore: checkpoint wipfeat: checkpoint plugin admin refactorfix: checkpoint comment provider boundary
The goal is to preserve a known-good recovery point between larger refactors instead of carrying one giant uncommitted worktree.
docs/ARCHITECTURE.mddocs/KERNEL.mddocs/PLUGINS.mddocs/THEMES.mddocs/EXTENSION_CONTRACTS.mddocs/SETUP_AND_RUNTIME_UPDATES.mddocs/SCHEDULER.mddocs/THEME_SANDBOX_CONTRACT.mddocs/MENUS.mddocs/MEDIA_MANAGER.mddocs/DATA_DOMAINS.mddocs/TRACING.mddocs/SECURITY_CI.mddocs/TESTING_DB.md
Current implementation includes:
- Typed plugin/theme contracts with manifest validation
- Runtime theme side-effect guardrails
- Runtime plugin capability enforcement
- Shared theme header/footer partial support
- Hash-based media object naming with variant derivation