Skip to content

TunkShif/gather

Repository files navigation

Gather

A private, single-user, link-bookmarking web app. Closer to Linkding than to Pocket — small, fast, honest. No in-app reader, no page preview, no body fetch. Gather remembers you wanted to read a link, what you called it, and what tags you filed it under. The page stays on the page.

Self-hosted, SQLite-backed, GitHub OAuth login, HMAC-signed cookie sessions. One configured owner; strangers get 403.

What Gather is not

  • Not a reader. No readability rendering, no article-body fetch.
  • Not a previewer. No thumbnails, no Open Graph image rendering.
  • Not multi-tenant. Single owner, gated by OWNER_GITHUB_ID. The code is CSV-ready so multi-owner is an env change, not a code change.
  • Not a native client. The web UI is responsive; offline via PWA is a post-MVP option, not a current feature.

If a future feature needs server-side body fetch, that's a different product. See docs/01-overview.md for the locked decision record.

Stack

  • Crystal 1.21, Kemal 1.12, SQLite via crystal-db + crystal-sqlite3
  • micrate for migrations (plain SQL files)
  • ECR via Kilt, server-rendered — no SPA, no JS framework
  • GitHub OAuth (read:user only), stateless HMAC-SHA256 cookie sessions
  • myhtml for metadata fetch only (used by fetch_metadata service)
  • No ORM, no DI container, no JWT shard — module singletons per docs/08-dependencies.md

Quick start (dev)

Requires: crystal (via mise — see mise.toml), just, sqlite3, openssl.

just bootstrap        # shards install, cp .env.example .env, mkdir db/migrations bin
$EDITOR .env          # fill GATHER_SESSION_SECRET, GitHub OAuth creds, OWNER_GITHUB_ID
just migrate          # apply migrations to db/gather.db
just dev              # run on http://localhost:3000

just auto-loads .env for every recipe. The app itself never reads .env — it reads ENV directly. In production the supervisor (systemd / fly / docker) injects env.

Find your GitHub numeric id

curl -H "Authorization: token <PAT>" https://api.github.com/user | jq .id

OWNER_GITHUB_ID uses the immutable id, not login (login can be renamed; id cannot).

Generate a session secret

just gen-secret       # openssl rand -hex 32 — paste into GATHER_SESSION_SECRET

Must be >= 32 bytes in production; Gather.settings.check_required! refuses to boot otherwise.

Deploy

Build a static binary:

just build            # shards build gather --release — output at bin/gather

Run with env vars from your supervisor (systemd unit, fly.toml, or docker --env-file). Apply migrations at deploy time or boot with MIGRATE=1 (dev convenience; not the production default).

SQLite file lives on a persistent volume; WAL tolerates crash-restart. Don't ship the SQLite file with the binary. HTTPS is via a reverse proxy (Caddy / Traefik) — Gather serves plain HTTP on localhost; the OAuth callback URL and Secure cookie flag assume HTTPS at the edge. Never expose Gather directly to the internet.

See docs/02-architecture.md for the deployment notes and the SQLite pragma rationale (journal_mode=WAL, synchronous=NORMAL, foreign_keys=ON, busy_timeout=5000).

Configuration

Every env key is documented in .env.example. The typed readers live in src/settings.cr. In production, missing required keys (GATHER_SESSION_SECRET, GATHER_GITHUB_CLIENT_ID, GATHER_GITHUB_CLIENT_SECRET, GATHER_OWNER_GITHUB_ID) cause Gather.settings.check_required! to Log.fatal and exit 1 — the app refuses to start in a broken state.

Project layout

gather/
├ shard.yml          deps and build target
├ justfile           dev recipes (spec, build, migrate, format, …)
├ .env.example       env keys with placeholder values
├ db/
│ ├── gather.db      gitignored runtime file
│ └ migrations/      micrate SQL files
├ src/
│ ├── gather.cr      module root, VERSION, requires
│   ├── settings.cr    typed ENV singleton, configure DSL
│ ├── db/            database.cr + repo/
│ ├── models/        DB::Serializable structs + converters
│ ├── services/      auth/, bookmarks/ (stateless orchestrators)
│ ├── middleware/    require_owner
│ ├── routes/        Kemal handlers
│ ├── helpers/       ECR mixins
│ ├── views/         ECR templates
│ └ assets/         app.css
└ spec/              in-memory SQLite, real repos, stub HTTP server

Tests

just spec             # full suite, ~10ms, in-memory SQLite
just spec-one spec/db/bookmarks_repo_spec.cr

No mocks at the DB seam — same repos, same SQL as production. Per-spec in-memory SQLite with micrate up at suite boot, truncate between tests. See docs/06-testing.md.

Documentation

docs/ holds the locked first-iteration MVP design record — start with docs/README.md. Each pivot has rationale so future changes have a baseline to compare against. docs/PROGRESS.md tracks the 12-step build sequence.

Contributing

  1. Fork it (https://github.com/TunkShif/gather/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Run just precommit (format-check, typecheck, spec)
  4. Commit your changes (git commit -am 'Add some feature')
  5. Push to the branch (git push origin my-new-feature)
  6. Create a new Pull Request

License

MIT — see LICENSE.

Contributors

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages