git clone https://github.com/flyingrobots/git-cms.git
cd git-cms
npm run setup # One-time: validates Docker prerequisites
npm run demo # See it in action!The safest way to try git-cms is in Docker, which provides complete isolation from your host system.
- Docker & Docker Compose installed
- 5 minutes of curiosity
# Clone the repository
git clone https://github.com/flyingrobots/git-cms.git
cd git-cms
# Run one-time setup (checks Docker prerequisites)
npm run setupnpm run demoThis shows you how git-cms works step-by-step.
npm run dev
# OR
docker compose up appWhat just happened?
- Docker built a containerized environment with Node 22 + Git
- Created an isolated Git repository inside the container
- Started the HTTP server on port 4638
Open your browser to: http://localhost:4638
You should see the Git CMS Admin interface with:
- Sidebar showing "Articles" and "Published"
- A form to create new articles
- Live preview of your content
-
In the admin UI, enter:
- Slug:
hello-world - Title:
My First Post - Body:
# Hello World This is my first article using Git as a CMS! ## How Cool Is This? Every save creates a Git commit. Every publish is an atomic ref update.
- Slug:
-
Click "Save Draft"
- Watch the terminal logs show the Git commit being created
- The article is now at
refs/_blog/articles/hello-world
-
Click "Publish"
- This fast-forwards
refs/_blog/published/hello-worldto match the draft - The article is now "live"
- This fast-forwards
# Open a shell inside the running container
docker compose exec app sh
# Create a draft
echo "# Hello World" | node bin/git-cms.js draft hello-world "My First Post"
# List all drafts
node bin/git-cms.js list
# Publish it
node bin/git-cms.js publish hello-world
# Read it back
node bin/git-cms.js show hello-world
# Exit the container
exitThe coolest part: this is all just Git under the hood.
# Enter the container
docker compose exec app sh
# Check what Git sees
git log --all --oneline --graph
# Look at the refs namespace
git for-each-ref refs/_blog/
# Read a commit message (this is your article!)
git log refs/_blog/articles/hello-world -1 --format="%B"
# Exit
exitWhat you'll see:
- Your article stored as a commit message
- Commits pointing to the "empty tree" (no files touched!)
- Refs acting as pointers to "current" versions
Git CMS uses low-level Git plumbing commands like:
git commit-tree(creates commits on empty trees)git update-ref(atomic ref updates)git hash-object(writes blobs directly)
While these operations are safe when used correctly, running tests or experiments on your host machine could:
- Create unexpected refs in your current repository
- Write test blobs to
.git/objects/ - Modify your Git configuration
Docker provides complete isolation - the container has its own filesystem, its own Git repos, and can be destroyed without a trace.
✅ Running in Docker: Completely safe. Destroy the container anytime with docker compose down -v
✅ Creating a dedicated test repo: If you want to try the CLI locally:
mkdir ~/git-cms-playground
cd ~/git-cms-playground
git init
# Now use git-cms here - it's isolated from your other repos❌ Running tests in your git-cms clone on host: Not recommended (see next section)
❌ Running git-cms commands in your active project repos: NEVER do this until you understand what's happening
Tests create and destroy temporary Git repositories. Always use Docker.
# Run all tests (automatically uses Docker)
npm test
# This is equivalent to:
./test/run-docker.sh
# Which runs:
docker compose run --rm testWhat the tests do:
- Create temporary repos in
/tmp/git-cms-test-* - Test all CRUD operations (create, read, update, publish)
- Test asset encryption and chunking
- Clean up afterward
Never run tests on your host unless you're comfortable with low-level Git operations.
If you want to use git-cms as a command-line tool on your host machine, you can install it globally - but only use it in dedicated Git repositories.
# From source (recommended until npm publish is complete):
cd git-cms
npm link
# After publish, global install will work:
# npm install -g git-cms# Create a fresh repo for your blog
mkdir ~/my-blog
cd ~/my-blog
git init
# Configure Git
git config user.name "Your Name"
git config user.email "you@example.com"
# Now use git-cms safely
echo "# My First Post" | git cms draft hello-world "Hello World"
git cms publish hello-worldCritical: Only use git cms commands in repositories where:
- You understand you're creating commits with empty trees
- You're okay with refs in
refs/_blog/*namespace - You've read the docs and understand what's happening
# Stop containers
docker compose down
# Stop containers AND delete all data (fresh start)
docker compose down -vnpm uninstall -g git-cms
# OR, if linked:
cd git-cms && npm unlink# If you created ~/my-blog for testing:
rm -rf ~/my-blogTraditional CMS architecture:
Article → JSON → POST /api → Parse → INSERT INTO posts → Database
Git CMS architecture:
Article → Commit Message → git commit-tree → .git/objects/ → Git
Every article commit points to Git's "empty tree" object. In SHA-1 repos this is 4b825dc642cb6eb9a060e54bf8d69288fbee4904, but in SHA-256 repos it is different.
# Derive the empty-tree OID for this repository's object format
git hash-object -t tree /dev/null
# Traditional Git commit
git add article.md # Stage file
git commit -m "Add article" # Commit references changed files
# Git CMS commit
git commit-tree <empty-tree-oid> -m "Article content here" # No files touched!This means:
- Your working directory stays clean
- All content lives in
.git/objects/and.git/refs/ - No merge conflicts from content changes
- Every save is a commit (infinite history)
# Draft ref points to latest commit
refs/_blog/articles/hello-world → abc123def...
# Publishing copies the pointer
refs/_blog/published/hello-world → abc123def...
# No new commits created!
# Atomic operation via git update-refOnce you're comfortable with the basics:
- Read the ADR (
docs/ADR.md) for deep architectural details - Try the Stargate Gateway (enforces fast-forward only + GPG signing)
./scripts/bootstrap-stargate.sh ~/git/_blog-stargate.git git remote add stargate ~/git/_blog-stargate.git git config remote.stargate.push "+refs/_blog/*:refs/_blog/*" git push stargate
- Experiment with encryption (see below)
- Explore installed package APIs in the project source (
src/lib/CmsService.js) and docs (docs/ADR.md)
Assets (images, PDFs) can be encrypted client-side before they touch Git.
# Generate a 256-bit key
openssl rand -base64 32
# Store in macOS Keychain
security add-generic-password -s git-cms-dev-enc-key -a $USER -w "<paste-key-here>"# Generate key
openssl rand -base64 32
# Store in GNOME Keyring (if available)
secret-tool store --label="Git CMS Dev Key" service git-cms-dev-enc-key
# Paste key when prompted# Inside Docker container
docker compose exec app sh
# Upload an encrypted file (base64 payload via HTTP API)
curl -X POST http://localhost:4638/api/cms/upload \
-H "Content-Type: application/json" \
-d '{"slug":"hello-world","filename":"image.png","data":"<base64>"}'
# The blob in Git is encrypted ciphertext
# Only you (with the key) can decrypt itSolution: Make sure Docker Desktop is running (macOS/Windows). On Linux, ensure the Docker daemon is running and your user has socket access (often via the docker group, distro-dependent):
# Linux example (group name may vary by distro)
sudo usermod -aG docker $USER
# Log out and back inIf group-based access is not configured, run Docker commands with sudo as a temporary workaround.
Solution: Change the port in docker-compose.yml:
ports:
- "5000:4638" # Maps localhost:5000 → container:4638Solution: reinstall npm dependencies and rebuild containers:
npm run check:deps
rm -rf node_modules
npm ci
docker compose build --no-cacheCause: You might be running tests on your host without Docker.
Solution: Always use:
npm test # Uses Docker automaticallyFor small personal blogs: Yes, with caveats. For high-traffic sites: No.
This is an educational project demonstrating Git's capabilities. Use it to:
- Learn Git internals
- Build prototype CMS systems
- Understand content-addressable storage
Don't use it for:
- Mission-critical applications
- Sites with >100 concurrent writers
- Anything requiring complex queries or full-text search
Yes! Use the git-stargate gateway to:
- Enforce fast-forward only (no force pushes)
- Verify GPG signatures
- Mirror to GitHub automatically
See: https://github.com/flyingrobots/git-stargate
Git's immutability conflicts with GDPR Article 17. Mitigation strategies:
- Use client-side encryption and delete keys (content becomes unreadable)
- Legal argument: journalistic/archival "legitimate interest"
- Don't store PII in articles
Consult a lawyer before using this for user-generated content in the EU.
That's the point. This is a "Git Stunt" - using Git in unconventional ways to understand:
- How content-addressable storage works
- How to build systems from first principles
- What Git's plumbing can actually do
You're supposed to walk away thinking "I would never use this in production, but now I understand Git (and databases) way better."
- Issues: https://github.com/flyingrobots/git-cms/issues
- Blog Series: https://flyingrobots.dev/posts/git-stunts
- ADR:
docs/ADR.md(comprehensive architecture docs)
Git CMS is a thought experiment that happens to work. It's designed to teach you how Git's plumbing works by building something that shouldn't exist.
If you're considering using this in production:
- Read the entire ADR (
docs/ADR.md) - Understand every decision and tradeoff
- Run it in Docker for at least a month
- Consider whether a traditional database might be... better
Then, if you're still convinced, go for it. Just remember: when you tell people you're using Git as your database, don't say I didn't warn you.
Have fun, and remember: "You know what? Have fun." — Linus (probably)