Self-hosted cloud note wall for transferring data between devices. PWA, Go + React, Docker.
  • TypeScript 46.7%
  • Go 28.9%
  • CSS 22.3%
  • HTML 1%
  • Dockerfile 0.6%
  • Other 0.5%
Find a file
eduard256 29d55be090
Fix ?since= comparison by parsing into time.Time (#2)
Strings stored by go-sqlite3 use space-separated format ('2026-05-18
12:49:21.342Z'), but ISO8601 from clients uses 'T' separator. String
comparison fails when formats mismatch.

Parse the since param into time.Time and pass through the driver, which
serializes it to the same format SQLite uses internally.

Accepts RFC3339Nano, RFC3339, 'YYYY-MM-DD HH:MM:SS', 'YYYY-MM-DD'.

Co-authored-by: Eduard <kazanveve09@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 15:51:36 +03:00
backend Fix ?since= comparison by parsing into time.Time (#2) 2026-05-18 15:51:36 +03:00
frontend Reapply "Add upload progress, lazy video loading, fix layout" 2026-03-31 11:30:48 +00:00
.dockerignore Add full application: Go backend + React frontend PWA 2026-03-31 08:59:59 +00:00
.env.example Add full application: Go backend + React frontend PWA 2026-03-31 08:59:59 +00:00
.gitignore Add full application: Go backend + React frontend PWA 2026-03-31 08:59:59 +00:00
docker-compose.yml Add full application: Go backend + React frontend PWA 2026-03-31 08:59:59 +00:00
Dockerfile Fix SQLite FTS5 build tag in Dockerfile 2026-03-31 09:27:06 +00:00
go.mod Add full application: Go backend + React frontend PWA 2026-03-31 08:59:59 +00:00
go.sum Add full application: Go backend + React frontend PWA 2026-03-31 08:59:59 +00:00
LICENSE Initial commit with MIT license and README 2026-03-31 08:42:50 +00:00
README.md Add self-hosted security rationale to README 2026-03-31 11:35:13 +00:00

Beaver Notes

Self-hosted note wall for transferring data between devices. Like Telegram Saved Messages, but better.

I built this because I needed a fast way to throw logs, passwords, configs, screenshots and files between my machines. Telegram splits large messages, has file size limits, and can't guarantee full security of your data -- it's stored on someone else's servers. With Beaver Notes, everything stays on your own machine. No third parties, no cloud, no trust required.

Features

  • Infinite message wall with virtual scroll
  • Full markdown rendering with syntax highlighting
  • Inline images with fullscreen preview
  • File uploads up to 40GB (any format)
  • Full-text search with filters (date, type, tags, file size)
  • Pin important messages
  • Drag & drop, clipboard paste
  • Real-time sync between devices (3s polling)
  • Auto dark/light theme (graphite/cream)
  • Password-only auth, 30-day sessions
  • PWA, installable on mobile
  • Single Docker container, SQLite inside

Quick Start

docker run -d \
  --name beaver-notes \
  -p 8762:8762 \
  -e AUTH_PASSWORD=your_password \
  -e JWT_SECRET=$(openssl rand -hex 32) \
  -v beaver-data:/data \
  eduard256/beaver-notes:latest

Open http://localhost:8762 in your browser.

Docker Compose

services:
  beaver-notes:
    image: eduard256/beaver-notes:latest
    ports:
      - "8762:8762"
    environment:
      - AUTH_PASSWORD=${AUTH_PASSWORD}
      - JWT_SECRET=${JWT_SECRET}
      - SECURE_COOKIE=${SECURE_COOKIE:-true}
    volumes:
      - beaver-data:/data
    restart: unless-stopped

volumes:
  beaver-data:
# Create .env
echo "AUTH_PASSWORD=your_password" > .env
echo "JWT_SECRET=$(openssl rand -hex 32)" >> .env

docker compose up -d

Environment Variables

Variable Required Default Description
AUTH_PASSWORD Yes - Login password
JWT_SECRET Yes - JWT signing key, min 32 chars
SECURE_COOKIE No true Set false for HTTP
PORT No 8762 Server port
DATA_DIR No /data Database and uploads directory

Important:

  1. SECURE_COOKIE=true requires HTTPS. If you access via HTTP, set it to false.
  2. Changing AUTH_PASSWORD invalidates all active sessions.
  3. JWT_SECRET must be at least 32 characters. Use openssl rand -hex 32 to generate.

Reverse Proxy

Caddy

notes.example.com {
    reverse_proxy localhost:8762
}

Nginx

server {
    server_name notes.example.com;
    client_max_body_size 40G;

    location / {
        proxy_pass http://localhost:8762;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
}

With a reverse proxy handling TLS, keep SECURE_COOKIE=true.

Backup

All data is in a single Docker volume. Database file + uploaded files.

# Stop the container
docker compose down

# Copy the volume
docker run --rm -v beaver-data:/data -v $(pwd):/backup alpine tar czf /backup/beaver-backup.tar.gz /data

# Start again
docker compose up -d

Tech Stack

  • Frontend: React, TypeScript, Vite, react-virtuoso, react-markdown
  • Backend: Go, net/http, SQLite with FTS5
  • Auth: JWT in HttpOnly Secure SameSite=Strict cookie
  • Deploy: Docker (multi-stage build)

Security

  • Constant-time password comparison
  • Rate limiting on login (5 attempts/minute)
  • Security headers (X-Frame-Options, CSP, X-Content-Type-Options)
  • UUID filenames, no path traversal
  • Parameterized SQL queries only
  • Session invalidation on password change

License

MIT