A self-hosted blogging platform built with Ruby on Rails 8.1 and the Solid stack. Prose runs entirely on SQLite — no Redis, Postgres, or external services required.
- Writing & Editing — Rich text with Lexxy, autosave, post scheduling, featured posts
- AI Assistant — Chat (proofread, critique, brainstorm), SEO/social metadata generation, featured image generation (Gemini/OpenAI), streaming responses
- MCP Server — Model Context Protocol endpoint for managing posts, categories, tags, and assets from Claude Desktop, Claude Code, or any MCP client
- REST API — Versioned JSON API at
/api/v1/for posts, categories, tags, site info, and assets, sharing the same bearer token as MCP - Webhooks — HMAC-signed outbound webhooks for post, subscriber, and comment events, with retries, a delivery log, and auto-disable on repeated failure
- Fediverse (ActivityPub) — Opt-in federation: Mastodon and other fediverse users can follow the site as
@blog@yourdomain, get new posts in their feeds, like them, and reply — replies arrive as comments held for moderation - Content Export — Download your whole site as a Markdown zip (one file per post/page with YAML front matter, plus images) or a full JSON backup including subscribers; exports run in the background from Admin → Export
- Content Organization — Categories, tags with searchable combo box and inline creation
- Reader Engagement — Comments with threading and moderation, loves, social share buttons, subscriber magic-link auth, email notifications
- Email Digests — Subscribers choose how often they hear about new posts at
/email-preferences(linked from every post email and the unsubscribe page): every new post, a weekly digest (Mondays 08:00), a monthly digest (the 1st, 08:00), or no post emails; newsletters are unaffected. Digests use the site's email branding and are skipped when nothing new was published - Multiple Newsletters (Mailing Lists) — Run several subscribable lists from one site (e.g. a weekly roundup and a deep-dives list) under Admin → Mailing Lists, with per-list subscriber counts. New subscribers join the lists marked "subscribe by default" and pick the rest on
/email-preferences. Each post is emailed to the lists checked in the editor's Settings tab the first time it's published — from the editor, the API, MCP, or the scheduler, and never twice — and digests only include posts from a subscriber's lists. Campaigns can target a single list, optionally narrowed by a segment - Reading List — Readers bookmark posts to
/reading-list; saved on the device with no account, and synced across devices once they subscribe and sign in - Social Embeds — X/Twitter and YouTube via oEmbed
- Analytics — Dashboard with view tracking, subscriber growth, post engagement
- Custom Pages — Static pages with rich text editor, top-level URLs (e.g.
/about) - Navigation Menus — Customize header links, footer links, and footer social icons from Admin → Navigation (drag-and-drop ordering, internal paths or external URLs, optional open-in-new-tab); social icons are picked automatically from the URL (X, GitHub, LinkedIn, Bluesky, Mastodon, YouTube, Instagram, and more)
- Customization — 30+ Google Fonts, adjustable typography, live preview
- Internationalization — Full i18n support with English and Spanish included; site-wide locale setting
- SEO — Slugged URLs, meta descriptions, RSS feed, XML sitemap
- Ruby 3.4.4 / Rails 8.1
- SQLite3 for all persistence
- Solid Queue — database-backed background jobs
- Solid Cache — database-backed caching
- Solid Cable — database-backed WebSockets
- RubyLLM — unified AI interface (Claude, Gemini, OpenAI)
- Hotwire (Turbo + Stimulus) — SPA-like interactivity
- Tailwind CSS — utility-first styling
- Propshaft — asset pipeline
- ImportMap — JavaScript modules without bundling
- Kamal — Docker-based deployment
- Ruby 3.4.4
- SQLite3
bin/setup # Install dependencies, prepare database, start server
bin/setup --skip-server # Setup without starting the serverbin/dev # Start dev server (Puma + Tailwind watcher) on port 3000Visit http://localhost:3000/admin/setup to create your admin account.
bin/rails test # Run all tests
bin/rails test:system # Run system (browser) testsbin/ci # Run full CI locally (setup, lint, security, tests)
bin/rubocop # Ruby linting
bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error # Security scan
bin/bundler-audit # Gem vulnerability audit
bin/importmap audit # JS dependency auditProse exposes a Model Context Protocol server at /mcp, allowing AI assistants to manage your blog programmatically. See docs/mcp_setup.md for the full setup guide.
-
Generate an API token from Admin > System > API Tokens
-
Connect your client:
Claude Code:
claude mcp add --transport http prose https://your-domain.com/mcp \ --header "Authorization: Bearer prose_YOUR_TOKEN"Claude Desktop connects through the
mcp-remotebridge (requires Node.js). Add this toclaude_desktop_config.json:{ "mcpServers": { "prose": { "command": "npx", "args": ["-y", "mcp-remote", "https://your-domain.com/mcp", "--header", "Authorization:${PROSE_AUTH_HEADER}"], "env": { "PROSE_AUTH_HEADER": "Bearer prose_YOUR_TOKEN" } } } }
Post management (list_posts, get_post, create_post, update_post, delete_post, publish_post, schedule_post, unpublish_post), site info (get_site_info, list_categories, list_tags, create_tag), and assets (upload_asset, set_featured_image).
Prose also exposes a versioned JSON REST API at /api/v1/, using the same prose_-prefixed bearer tokens as MCP, for integrations that don't speak MCP (mobile apps, custom frontends, data pipelines). See docs/api_setup.md for the full endpoint reference.
curl https://your-domain.com/api/v1/posts \
-H "Authorization: Bearer prose_YOUR_TOKEN"This project uses GitHub Flow. The master branch is always deployable — all work happens on feature branches and is merged via pull request.
- Create a branch from
master:git checkout master && git pull git checkout -b my-feature-branch - Make your changes and verify they pass all checks:
bin/rails test # All unit tests must pass bin/rubocop # All linting must pass
- Commit, push, and open a pull request:
git push -u origin my-feature-branch gh pr create
- All unit tests pass (
bin/rails test) - No RuboCop offenses (
bin/rubocop) - Security scans clean (
bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error) - One logical change per PR
Prose deploys as a Docker container via Kamal. SQLite databases are persisted through a Docker volume mount. Solid Queue runs in-process with Puma.
- A VPS or dedicated server with Docker installed (Ubuntu 22.04+ recommended)
- A domain name pointed at your server's IP address
- SSH access to the server as root (or a user with Docker privileges)
- A container registry account (Docker Hub, GitHub Container Registry, etc.) — or use a local registry
Optional:
- SMTP credentials for email delivery (subscriber notifications, magic links)
- S3-compatible storage for file uploads (AWS S3, DigitalOcean Spaces, Cloudflare R2, MinIO)
Edit the deployment configuration for your environment:
# Set your server IP
servers:
web:
- YOUR_SERVER_IP
# Enable SSL with Let's Encrypt (uncomment and set your domain)
proxy:
ssl: true
host: yourdomain.com
# Configure your container registry
registry:
server: ghcr.io # or hub.docker.com, registry.digitalocean.com
username: your-username
password:
- KAMAL_REGISTRY_PASSWORD
# Set your domain and any optional services
env:
clear:
SOLID_QUEUE_IN_PUMA: true
APP_HOST: yourdomain.com
# SMTP_ADDRESS: smtp.example.com
# SMTP_PORT: 587
# SMTP_USERNAME: your-username
# SMTP_FROM: noreply@yourdomain.com
# ACTIVE_STORAGE_SERVICE: amazonGenerate the required production secrets:
bin/rails prose:generate_secretsSave the output to .kamal/.env (this file is gitignored):
# .kamal/.env
SECRET_KEY_BASE=<generated value>
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=<generated value>
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=<generated value>
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=<generated value>Then update .kamal/secrets to source them:
source .kamal/.envWarning: These secrets encrypt your AI API keys and other sensitive data. If lost, encrypted data becomes unrecoverable. Back them up securely. Do not change
SECRET_KEY_BASEafter deployment — it is used for IP anonymization in analytics.
If using GitHub Container Registry:
# Add to .kamal/.env
KAMAL_REGISTRY_PASSWORD=ghp_your_github_token
# Uncomment in .kamal/secrets
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORDkamal setup # First deploy — provisions server, pushes image, starts containers- Visit
https://yourdomain.com/admin/setupto create your admin account - Configure your site name and settings in Admin > System > Site Settings
- Add AI API keys (Anthropic, OpenAI, Google) in site settings to enable AI features
Set these environment variables in config/deploy.yml under env.clear (and SMTP_PASSWORD is already in env.secret):
env:
clear:
SMTP_ADDRESS: smtp.example.com
SMTP_PORT: 587
SMTP_USERNAME: your-username
SMTP_FROM: noreply@yourdomain.comRecurring email jobs (post digests, scheduled newsletters) run on the schedules in config/recurring.yml. Cron times use the server time zone, which is UTC in the Docker image.
For file uploads stored in S3 instead of local disk:
- Uncomment
gem "aws-sdk-s3"in theGemfileand runbundle install - Set environment variables:
env:
clear:
ACTIVE_STORAGE_SERVICE: amazon
AWS_REGION: us-east-1
AWS_BUCKET: your-bucket-name
# AWS_ENDPOINT: https://your-endpoint.com # For S3-compatible services
secret:
- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEYAdmin users can register passkeys (Touch ID, Face ID, security keys) for passwordless sign-in. Passkeys work out of the box in development. For production, set the origin and relying party ID to match your domain:
env:
clear:
WEBAUTHN_ORIGIN: https://yourdomain.com
WEBAUTHN_RP_ID: yourdomain.comManage passkeys at /admin/passkeys after signing in.
Turn on Admin → Settings → Fediverse to let Mastodon, Threads, and other fediverse users follow your site. Pick a username (default blog); people follow @blog@yourdomain.com. The site serves WebFinger at /.well-known/webfinger and an ActivityPub actor, inbox, and outbox under /activitypub/.
- Publishing a post delivers it to followers; editing or unpublishing it sends an update or a delete. Members-only and paid posts are shared only as a teaser and a link.
- Fediverse likes appear on each post's admin dashboard. Replies become comments held for moderation at
/admin/comments, labeled "Fediverse". APP_HOSTmust be set to your permanent public domain, and the site must be served over HTTPS, before you enable federation. Remote servers store the actor and post URLs built from it, so changing the domain later breaks existing follows.
| Variable | Required | Default | Description |
|---|---|---|---|
SECRET_KEY_BASE |
Yes | — | Rails secret key for sessions and signed cookies |
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY |
Yes | — | Encrypts sensitive model attributes (AI API keys) |
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY |
Yes | — | Deterministic encryption for queryable fields |
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT |
Yes | — | Salt for encryption key derivation |
APP_HOST |
No | example.com |
Your domain name (enables host authorization; required for fediverse federation) |
RAILS_ASSUME_SSL |
No | true |
Set to false if not using SSL |
SOLID_QUEUE_IN_PUMA |
No | true |
Run background jobs in the web process |
ACTIVE_STORAGE_SERVICE |
No | local |
Storage backend: local or amazon |
SMTP_ADDRESS |
No | — | SMTP server address (enables email delivery) |
SMTP_PORT |
No | 587 |
SMTP server port |
SMTP_USERNAME |
No | — | SMTP authentication username |
SMTP_PASSWORD |
No | — | SMTP authentication password |
SMTP_FROM |
No | noreply@example.com |
Default sender email address |
SMTP_DOMAIN |
No | APP_HOST |
HELO domain for SMTP |
SMTP_AUTHENTICATION |
No | plain |
SMTP auth method (plain, login, cram_md5) |
WEB_CONCURRENCY |
No | 1 |
Number of Puma worker processes |
JOB_CONCURRENCY |
No | 1 |
Number of Solid Queue worker threads |
WEBAUTHN_ORIGIN |
No | http://localhost:3000 |
Full origin URL for passkey/WebAuthn verification |
WEBAUTHN_RP_ID |
No | localhost |
Relying Party ID for passkeys (usually your domain) |
RAILS_LOG_LEVEL |
No | info |
Log verbosity (debug, info, warn, error) |
kamal deploy # Build, push, and deploy the latest codekamal console # Open Rails console on the server
kamal shell # Open bash shell in the container
kamal logs # Tail application logs
kamal details # Show running containers and health statusThis project is licensed under the MIT License. See LICENSE for details.