A Markdown-first, self-hosted document repository.
Find a file
2026-07-06 19:21:42 -06:00
migrations feat(db): add base schema for Cantilever with core tables and initial data 2026-06-22 19:53:14 -06:00
src feat(views): add public-facing page rendering templates 2026-07-06 19:21:42 -06:00
.gitignore feat: initialize project with package.json, tsconfig.json, and vitest configuration 2026-06-22 21:18:56 -06:00
eslint.config.mjs feat: initialize project with package.json, tsconfig.json, and vitest configuration 2026-06-22 21:18:56 -06:00
LICENSE Initial Commit 2026-06-22 19:20:03 -06:00
package-lock.json fix: add markdown-it and its types to dependencies 2026-06-22 21:30:47 -06:00
package.json fix: add markdown-it and its types to dependencies 2026-06-22 21:30:47 -06:00
README.md Initial Commit 2026-06-22 19:20:03 -06:00
tsconfig.json feat: initialize project with package.json, tsconfig.json, and vitest configuration 2026-06-22 21:18:56 -06:00
vitest.config.ts feat: initialize project with package.json, tsconfig.json, and vitest configuration 2026-06-22 21:18:56 -06:00

Cantilever

A Markdown-first self-hosted document repository — a digital reading room for long-form writing. Write in Markdown, organize with collections and series, publish on a schedule, and serve a beautiful public site with full-text search, RSS/Atom feeds, threaded comments, and citation generation.

Features

  • Markdown-first: Write your documents in Markdown, and let Cantilever handle the rest. No need to worry about formatting (including KaTeX!) or styling — just focus on your content.
  • Document lifecycle: Private --> Unlisted --> Scheduled --> Published. Control who can see your documents and when they go live.
  • Full-text search: Search accross all your documents with SQLite FTS5, with a LIKE fallback for malformed queries.
  • Git-based versioning: Every change to your documents is tracked with Git, allowing you to revert to previous versions and maintain a history of your work (and optionally, share changes publicly).
  • Static site export: Export your entire document repository as a static site, ready to be hosted on any platform that serves static files (e.g., GitHub Pages, Netlify, Vercel).
  • Citation generation: Generate citations for your documents in various formats (e.g., APA, MLA, Chicago, BibTeX) with a single click.
  • Webhooks: Integrate with other services (e.g., Zapier, IFTTT) to automate workflows based on document events.
  • Threaded comments: Allow readers to engage with your content through threaded comments, with optional email notifications for moderation.
  • RSS/Atom feeds: Keep your readers updated with RSS/Atom feeds for your collections and series.
  • Dark mode: A sleek dark mode for comfortable reading in low-light environments.
  • Admin SPA: A single-page application for managing your document repository, including creating and editing documents, organizing collections and series, and configuring settings.
  • Custom pages: Create standalone pages (About, Contact, etc.) in Markdown, served alongside your documents.
  • API keys: Create read-only API keys for programmatic access to the public API.
  • JSON import/export: Bulk export and import your entire repository as a single JSON file — perfect for backups and migration.
  • Sitemap: Auto-generated sitemap.xml for search engine discovery.
  • Random discovery: /random redirects to a random public document.
  • Fully dockerized: Easy to deploy and manage with Docker, ensuring a consistent environment across different platforms.

Architecture

Cantilever runs two Fastify servers on separate ports by default (configurable):

Server Default Port Purpose
Public 3234 SSR pages, public API, RSS/Atom feeds
Admin 3235 Admin SPA, content CRUD, settings

Both servers can run on the same port when ADMIN_PORT is set to the same value as PUBLIC_PORT. In same-port mode, the admin interface is protected by ADMIN_PASSWORD.

Getting Started

With docker-compose:

services:
  cantilever:
    image: marvilco/cantilever:latest
    container_name: cantilever
    ports:
      - "3234:3234" # Public server
      - "3235:3235" # Admin server
    volumes:
      - ./cantilever/db:/app/data/db
      - ./cantilever/files:/app/data/files
    environment:
      - PUBLIC_URL=https://yourdomain.com
      - ADMIN_PASSWORD=your_admin_password
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3234/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s

Configuration

All configuration is via environment variables:

Variable Default Description
PUBLIC_PORT 3234 Public server port
ADMIN_PORT 3235 Admin server port
PUBLIC_URL (empty) Public URL (e.g., https://yourdomain.com) for generating links and feeds
LOG_LEVEL info Logging level (trace, debug, info, warn, error, fatal)
ADMIN_PASSWORD (empty) Admin password (required if ADMIN_PORT is the same as PUBLIC_PORT)
SMTP_HOST (empty) SMTP server hostname for email notifications
SMTP_PORT 587 SMTP server port
SMTP_USER (empty) SMTP username
SMTP_PASS (empty) SMTP password
SMTP_FROM (empty) From address for notification emails

API Overview

Public API (port 3234)

Method Path Description
GET / Homepage (SSR)
GET /search?q=... Search results page (SSR)
GET /collections Collections listing (SSR)
GET /series Series listing (SSR)
GET /random Redirect to a random document
GET /:slug Document or custom page (SSR)
GET /feed.xml RSS feed
GET /feed.atom Atom feed
GET /sitemap.xml Sitemap
GET /thumb/:slug.png OG image thumbnail
GET /health Health check
GET /api/documents List public documents (JSON)
GET /api/documents/:slug Single document (JSON)
GET /api/documents/:slug/versions Version history (if enabled)
GET /api/documents/:slug/diff Diff between versions
GET /api/search?q=... Search results (JSON)
GET /api/collections List collections (JSON)
GET /api/series List series (JSON)
GET /api/pages List public pages (JSON)
GET /api/pages/:slug Single page (JSON)
GET /api/comments/:documentId Get comments for a document
POST /api/comments/:documentId Submit a comment
GET /api/citations/:documentId Generate citations

Admin API (port 3235)

Method Path Description
GET/POST /api/admin/documents List / Create documents
GET/PUT/DELETE /api/admin/documents/:id Get / Update / Delete document
GET /api/admin/documents/:id/versions Git version history
GET /api/admin/documents/:id/versions/:hash Specific version content
POST /api/admin/documents/:id/versions/:hash/revert Revert to version
GET/POST /api/admin/collections List / Create collections
GET/PUT/DELETE /api/admin/collections/:id Get / Update / Delete collection
POST /api/admin/collections/:id/documents Add document to collection
DELETE /api/admin/collections/:id/documents/:docId Remove document from collection
GET/POST /api/admin/series List / Create series
GET/PUT/DELETE /api/admin/series/:id Get / Update / Delete series
POST /api/admin/series/:id/documents Add document to series
DELETE /api/admin/series/:id/documents/:docId Remove document from series
PUT /api/admin/series/:id/reorder Reorder documents in series
GET /api/admin/comments/pending Pending moderation queue
POST /api/admin/comments/:id/approve Approve a comment
DELETE /api/admin/comments/:id Reject / Delete a comment
POST /api/admin/comments Create admin-authored comment
GET/POST /api/admin/licenses List / Create licenses
GET/PUT/DELETE /api/admin/licenses/:id Get / Update / Delete license
GET/POST /api/admin/webhooks List / Create webhooks
GET/PUT/DELETE /api/admin/webhooks/:id Get / Update / Delete webhook
GET/PUT /api/admin/settings Get all / Update settings
GET/POST /api/admin/pages List / Create custom pages
GET/PUT/DELETE /api/admin/pages/:id Get / Update / Delete page
GET/POST /api/admin/api-keys List / Create API keys
DELETE /api/admin/api-keys/:id Revoke an API key
POST /api/admin/images/upload Upload an image
POST /api/admin/images/unzip Extract a zip of images
POST /api/admin/export Trigger static site export
GET /api/admin/export/status Export status check
GET /api/admin/export/json Download full JSON export
POST /api/admin/import/json Import from JSON export

Tech Stack

Component Technology
Runtime Node.js 22
Language TypeScript 5.7 Strict
Web Framework Fastify 5
Database better-sqlite3 11 WAL
ORM Drizzle ORM
Search SQLite FTS5
Git Integration simple-git
Syntax Highlight highlight.js
Email nodemailer
Logging Pino + pino-pretty
Image Processing Sharp + text-to-svg
Testing Vitest + Playwright
Linting ESLint 9
Containerization Docker

Design Decisions

  • Drizzle ORM: Typed queries with zero runtime overhead. Schema is the single source of truth, migrations are plain .sql files.
  • No frontend framework: SSR uses template literal functions. Admin is vanilla JS. Zero build step for client-side code.
  • Optional port separation: By default, Cantilever runs the public and admin servers on separate ports for added security (opening the public port does not expose the admin interface). Both servers can run on the same port — in this mode, ADMIN_PASSWORD protects the admin interface.
  • No external search engine: Cantilever implements full-text search using FTS5 in SQLite, with a LIKE fallback for malformed queries. This keeps the architecture simple and self-contained.
  • Per-document Git repositories: Each document is stored in its own Git repository (via simple-git), allowing for granular version control and history tracking.
  • WAL mode for SQLite: Cantilever uses Write-Ahead Logging (WAL) mode for SQLite to improve concurrency and performance.

License

Cantilever is licensed under the GNU Affero General Public License v3.0.