A self-hosted note-taking application inspired by Google Keep. Built with Ruby on Rails, SQLite, and Hotwire, with companion mobile apps via Turbo Native.
- Rich editing — Toggle between WYSIWYG (Tiptap/ProseMirror) and raw Markdown; content stored as Markdown
- Organization — Pin, archive, and tag notes with colored labels
- Full-text search — SQLite FTS5-powered search across titles and bodies
- Version history — Automatic snapshots on every save; view diffs and restore previous versions
- Sharing — Share notes read-write with other users; revocable by the owner
- Attachments — Upload files, images, and videos (stored locally via Active Storage, 25 MB default limit)
- Soft delete — Trashed notes are permanently deleted after 30 days
- Export — Download individual notes or bulk-export as Markdown files
- REST API — Complete JSON API at
/api/v1/with token-based auth, pagination, rate limiting, and OpenAPI docs at/api/docs - Authentication — Google OAuth2 for web sessions; email/password with token-based auth for API and mobile clients
- Admin panel — User management and platform settings for administrators
- Responsive design — Card-based layout that works well on desktop and in Turbo Native mobile shells
- Ruby 4.0.1 (see
web/.ruby-version) - Bundler (ships with Ruby)
- SQLite 3.x with development headers
- libvips (for image processing / Active Storage variants)
- Node.js (not required — asset pipeline uses importmap)
- Go 1.23+ (only for the optional
cmd/import-memostool)
On Debian/Ubuntu:
sudo apt-get install sqlite3 libsqlite3-dev libvipsnotes/
├── AGENTS.md # Architecture and design specification
├── README.md # This file
├── web/ # Rails application
│ ├── app/ # Models, controllers, views, jobs, assets
│ ├── config/ # Rails configuration, routes, deploy config
│ ├── db/ # Migrations, schema, seeds
│ ├── spec/ # RSpec test suite
│ ├── Dockerfile # Production container image
│ ├── Gemfile # Ruby dependencies
│ └── Procfile.dev # Foreman process definitions for development
└── cmd/
└── import-memos/ # Go CLI tool to migrate data from Memos
cd web
bin/setupThis installs gem dependencies, creates the SQLite database, and runs migrations. A default admin user is seeded (mlbright@gmail.com / admin).
cd web
bin/devThis starts the Rails server and Tailwind CSS watcher via Foreman. The app is available at http://localhost:3000.
Alternatively, start the Rails server alone:
cd web
bin/rails server| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
HTTP port for the development server |
RAILS_ENV |
development |
Rails environment (development, test, production) |
RAILS_MASTER_KEY |
— | Decrypts config/credentials.yml.enc (required in production) |
WEB_CONCURRENCY |
1 |
Number of Puma worker processes |
RAILS_MAX_THREADS |
5 |
Threads per Puma worker / max DB connections |
SOLID_QUEUE_IN_PUMA |
true |
Run Solid Queue background jobs inside the Puma process |
The test suite uses RSpec with FactoryBot:
cd web
# Run the full test suite
bundle exec rspec
# Run a specific spec file
bundle exec rspec spec/models/note_spec.rb
# Run a specific test by line number
bundle exec rspec spec/requests/api/v1/notes_spec.rb:42cd web
bin/ciThis runs the full CI pipeline:
bin/setup— Install dependencies and prepare the databasebin/rubocop— Ruby style checks (Standard/Rails Omakase)bin/bundler-audit— Gem vulnerability auditbin/importmap audit— JavaScript dependency auditbin/brakeman— Static security analysis
cd web
# Build the production image
docker build -t notes .
# Run the container
docker run -d \
-p 80:80 \
-e RAILS_MASTER_KEY=<value-from-config/master.key> \
-v notes_storage:/rails/storage \
--name notes \
notesThe Dockerfile uses a multi-stage build:
- Build stage — Installs gems, precompiles bootsnap and assets
- Runtime stage — Minimal image with the compiled app, runs as non-root user
- Entrypoint — Automatically runs pending migrations on startup
- Server — Puma behind Thruster (HTTP compression + asset caching), exposed on port 80
If deploying without Docker:
cd web
RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 bin/rails assets:precompileThe project includes a Kamal configuration at web/config/deploy.yml:
cd web
# First-time setup
bin/kamal setup
# Deploy
bin/kamal deploy
# Open a Rails console on the server
bin/kamal console
# Tail logs
bin/kamal logsEdit config/deploy.yml to configure:
- Server IP — Update
servers.webwith your VPS address - Registry — Point to your Docker registry
- Environment — Set
RAILS_MASTER_KEYand other secrets in.kamal/secrets
Target: Ubuntu/Debian VPS with Caddy + systemd.
-
Install dependencies:
sudo apt-get install ruby sqlite3 libsqlite3-dev libvips
Install Caddy following the official instructions.
-
Clone and set up:
git clone <repo-url> /opt/notes cd /opt/notes/web RAILS_ENV=production bin/setup
-
Set the master key:
# Copy from your local machine: scp config/master.key server:/opt/notes/web/config/master.key # Or set as an environment variable: export RAILS_MASTER_KEY=<your-key>
-
Precompile assets:
RAILS_ENV=production bin/rails assets:precompile
-
Create a systemd service (
/etc/systemd/system/notes.service):[Unit] Description=Notes (Puma) After=network.target [Service] Type=simple User=deploy WorkingDirectory=/opt/notes/web Environment=RAILS_ENV=production Environment=RAILS_MASTER_KEY=<your-key> Environment=SOLID_QUEUE_IN_PUMA=true ExecStart=/opt/notes/web/bin/thrust /opt/notes/web/bin/rails server Restart=always [Install] WantedBy=multi-user.target
-
Configure Caddy as a reverse proxy (
/etc/caddy/Caddyfile):notes.example.com { reverse_proxy 127.0.0.1:3000 }
Caddy automatically provisions and renews TLS certificates via Let's Encrypt — no separate certbot step needed.
-
Backups — Use Litestream for continuous SQLite replication:
litestream replicate /opt/notes/web/storage/production.sqlite3 s3://bucket/notes/
The REST API is available at /api/v1/. Documentation is served at /api/docs.
# Obtain a token
curl -X POST http://localhost:3000/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "secret"}'
# Use the token
curl http://localhost:3000/api/v1/notes \
-H "Authorization: Bearer <token>"API requests are rate-limited to 3000 requests per 5 minutes per IP address and per API token. Exceeding the limit returns HTTP 429 with a Retry-After header.
A standalone Go CLI at cmd/import-memos/ migrates data from a Memos instance into Notes.
cd cmd/import-memos
go build -o import-memos ../import-memos \
--memos-url https://memos.example.com \
--memos-token "$(cat ~/.memo-token)" \
--notes-url http://localhost:3000 \
--delay 200| Flag | Required | Description |
|---|---|---|
--memos-url |
Yes | Base URL of the Memos instance |
--memos-token |
Yes | Personal Access Token for Memos |
--notes-url |
Yes | Base URL of the Notes instance |
--delay |
No | Milliseconds to wait between Notes API calls (default: 0) |
--dry-run |
No | Preview what would be imported without writing |
The tool interactively prompts for Notes user credentials to map Memos users to Notes accounts. It migrates:
- Memo content (with H1 headings extracted as note titles)
- Tags (created if they don't exist, default gray color)
- Pinned / archived state
- Attachments (files up to 25 MB)
- Original created/updated timestamps
- Memo relations, reactions, and comments are not migrated
- Visibility settings have no equivalent — all imported notes are private
- Shares are not migrated
- Tag colors default to gray (
#6b7280)
Private — all rights reserved.