Skip to content

paulwellnerbou/marginalia

Repository files navigation

Marginalia

Markdown → beautiful themed HTML, with a collaborative viewer.

Documents, comments, invites, sessions, and assets are persisted on the server. The browser only stores local auth/session helpers and UI state such as invite tokens, display name, recent docs, and theme.

See REQUIREMENTS.md and PLAN.md.

Layout

  • packages/renderer — core library: Markdown → HTML + metadata
  • packages/climarginalia CLI
  • packages/element<marginalia-doc> web component (Shadow DOM)
  • packages/react — thin React wrapper
  • packages/themes — CSS themes
  • apps/server — Hono + Bun collaborative server
  • apps/web — Vite + React SPA viewer/editor

Development

bun install
bun test

# Dev servers (server on :3434, Vite on :5173)
bun run dev

# Production build + runtime
bun run build
bun run start

Document assets

Documents reference images and (planned) include files by a name that appears in the source — ![cat](cat.png) in markdown, image::cat.png[] in AsciiDoc. Those names resolve against a per-document asset store.

  • Uploads happen through the editor: drop or paste an image into the editor pane, or click the dropzone that replaces a missing-image reference. The file is bound to the exact name used in source.
  • Viewers request assets through /api/documents/:uid/assets/:refName, which goes through the same authorization check as the document itself — a user without access to the document cannot fetch its assets, even with a direct URL.
  • Blobs are content-addressed (sha256). The same bytes uploaded under two different names — or to two different documents — are stored once. Detaching the last reference garbage-collects the blob.

Asset endpoints (all gated by per-document authz; writes require editor+):

  • GET /api/documents/:uid/assets — list attached assets
  • POST /api/documents/:uid/assets — multipart upload (file, ref_name, optional kind)
  • GET /api/documents/:uid/assets/:refName — fetch bytes (ETag + Cache-Control: private, max-age=0, must-revalidate so access is re-checked on every hit; non-image mimes are served as Content-Disposition: attachment)
  • DELETE /api/documents/:uid/assets/:refName — detach (and GC the blob if nothing else references it)

Upload size defaults to 16 MiB; override with maxAssetBytes in config.ts.

Blob storage backend

Two backends; same interface, one config switch.

Filesystem (default, zero-config). Blobs land under MARGINALIA_DATA_DIR/blobs/<sha[0:2]>/<sha>. Good for self-hosting, Docker volumes, and local development.

S3-compatible (any of AWS S3, Cloudflare R2, MinIO, Backblaze B2, DigitalOcean Spaces…). Enable with MARGINALIA_BLOB_STORAGE=s3 plus:

MARGINALIA_BLOB_STORAGE=s3
MARGINALIA_S3_BUCKET=marginalia-blobs
MARGINALIA_S3_ACCESS_KEY_ID=...
MARGINALIA_S3_SECRET_ACCESS_KEY=...
# Optional — AWS S3 picks the right endpoint from the region if omitted.
MARGINALIA_S3_ENDPOINT=http://localhost:9000       # e.g. MinIO
MARGINALIA_S3_REGION=auto
MARGINALIA_S3_PREFIX=prod/blobs/                   # key prefix inside the bucket
MARGINALIA_S3_VIRTUAL_HOSTED=1                     # if your endpoint needs it

Credentials must be readable at startup; the server fails loudly if any required S3 var is missing. All reads still go through the per-document proxy — never share bucket credentials or pre-signed URLs with end users.

JSON Bundles

Documents can be exported and imported as versioned JSON bundles through the server API:

  • GET /api/documents/:uid/export downloads a .marginalia.json bundle
  • POST /api/documents/import creates a new document from a previously exported bundle

The bundle includes:

  • document metadata and markdown source
  • comment threads
  • renderer metadata (frontmatter, TOC, assets, mermaid blocks, block map, warnings)

That makes the export readable by external tools while still round-tripping back into Marginalia.

Deployment

This repo includes Docker-based deployment automation:

Runtime environment

These are the main runtime env vars the container understands:

  • PORT — HTTP listen port inside the container. Default: 3434
  • MARGINALIA_DATA_DIR — persistent data directory. Default: repo-root .data/ in local dev, /app/.data/ in Docker
  • MARGINALIA_WEB_DIR — built SPA directory. Default: /app/apps/web/dist
  • APP_ENV_LABEL — optional label appended to the browser title, e.g. DEV
  • MARGINALIA_BLOB_STORAGEfs (default) or s3. See Blob storage backend for the S3 env vars.

GitHub setup

If you use the bundled GitHub Actions deploy workflows, configure:

  • Secrets: VPS_HOST, VPS_USER, VPS_SSH_PRIVATE_KEY, DEPLOY_PATH
  • Variable: DOMAIN

On the deployment host, the deploy script looks for:

  • $DEPLOY_PATH/.env.dev
  • $DEPLOY_PATH/.env.prod

Those files can define the runtime env vars above, plus optional deploy-time overrides like:

  • HOST_PORT — host port to publish the container on
  • HOST_BIND_IP — defaults to 127.0.0.1
  • CONTAINER_NETWORK — optional Docker network name

Local Docker

docker build -t marginalia .
docker run -d \
  --name marginalia \
  -p 3434:3434 \
  -v "$PWD/.data:/app/.data" \
  -e APP_ENV_LABEL=DEV \
  -e MARGINALIA_DATA_DIR=/app/.data \
  -e MARGINALIA_WEB_DIR=/app/apps/web/dist \
  marginalia

Server-side state

All persistent server state lives in a single directory — the repo-root .data/ by default in local development, or the path in MARGINALIA_DATA_DIR (see config.ts):

.data/
├── db.sqlite          SQLite DB: documents, invites, sessions, doc_users,
│                       comments, comment_mentions, edit_proposals,
│                       assets, document_assets
├── db.sqlite-wal      WAL file (journal mode)
├── db.sqlite-shm      Shared-memory index for the WAL
├── repo/              Git repo holding every document as <uid>.md
└── blobs/             Content-addressed asset binaries (FS backend only;
                        absent when MARGINALIA_BLOB_STORAGE=s3)

Clear everything

Stop the server, then delete the data directory:

rm -rf .data/

Next startup recreates the directory, an empty SQLite schema, and a fresh git init. Every document, invite, and comment is gone.

To reset while preserving the git history for manual inspection, delete only the DB files:

rm -f .data/db.sqlite .data/db.sqlite-wal .data/db.sqlite-shm

Note that the git repo references documents by uid; dropping the DB orphans the .md files (they're no longer accessible via any URL). The blobs/ directory is similarly orphaned — those files are addressable only through the assets / document_assets tables, so once the DB is gone they're dead weight and safe to remove.

Reset client state (invite tokens, display name, recent docs, theme)

This only clears browser-held auth/session helpers and UI state. It does not remove any server-stored documents, comments, history, invites, or assets.

Everything the web app persists locally lives in localStorage under the marginalia.* prefix. Password-protected docs also use the marginalia_session cookie. Or just wipe the site in browser settings.

About

Webapp, libary and web component to render markdown beautifully and collaborate on markdown documents

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages