Commit your thoughts to source.
A git-native publishing platform for developers. Every post is a markdown file in your own GitHub repo — no lock-in, no export button, no middleman. Write from the browser, vim, vscode, or anywhere that speaks markdown.
→ essay.sh
curl -fsSL essay.sh/install | shOr download a binary directly from releases. Supports macOS, Linux, and Windows (amd64 + arm64).
essay auth # opens browser, copies code to clipboard
essay new "On the nature of side projects" # opens $EDITOR
essay publish on-the-nature-of-side-projectsYour post is now live at essay.sh/username/on-the-nature-of-side-projects.
- Sign in at essay.sh with GitHub
- Connect a GitHub repo — the app only ever accesses that one repo
- Run
essay authto authenticate the CLI - Write from the web editor or the CLI — both commit markdown to your repo
- Posts are served at
essay.sh/username/slug
Every post is stored as posts/{slug}.md with YAML frontmatter in your repo. You own it forever.
Authenticate with your essay.sh account via GitHub device flow. The confirmation code is copied to your clipboard and the browser opens automatically.
Create a new post. Opens $EDITOR with a blank file. If no title is given, a random slug is generated.
essay new
essay new "Thoughts on Postgres"List all your posts.
SLUG TITLE VISIBILITY
thoughts-on-postgres Thoughts on Postgres public
rough-draft Rough draft private
Fetch a post and open it in $EDITOR. Accepts slug, full ID, or ID prefix.
essay edit thoughts-on-postgres
essay edit abc123Toggle a post between public and private.
essay publish thoughts-on-postgresThe editor at essay.sh/dashboard/new supports:
- Markdown editing with live preview
- Vim keybindings — toggle via
···menu (top right) - Public / private visibility toggle
- Custom or auto-generated slugs
⌘Sto save,⌘↵to publish:wand:wqin vim mode
| layer | tech |
|---|---|
| web | Next.js 16 (App Router, ISR) |
| auth | Auth.js v5 + GitHub OAuth + CLI bearer tokens |
| database | Prisma 7 + PostgreSQL (Neon) |
| post storage | GitHub (via App installation tokens) |
| cli | Go 1.23 + Cobra + Viper |
| terminal ui | Charmbracelet lipgloss |
| hosting | Vercel |
| cli releases | goreleaser + release-please |
Prerequisites: Node 20+, pnpm, Go 1.23+, Docker
git clone https://github.com/timmywheels/essay
cd essay
# web
cd web
cp .env.local.example .env.local # fill in values below
docker compose up -d # start postgres
pnpm install
npx prisma migrate deploy
pnpm dev # http://localhost:3000
# cli (separate terminal)
cd cli
export ESSAY_GITHUB_CLIENT_ID="<your-dev-app-client-id>"
export ESSAY_API="http://localhost:3000"
make install # installs to $GOPATH/bin
essay auth| variable | description |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
AUTH_SECRET |
openssl rand -base64 32 |
AUTH_GITHUB_ID |
GitHub App OAuth client ID |
AUTH_GITHUB_SECRET |
GitHub App OAuth client secret |
GITHUB_APP_ID |
GitHub App numeric ID |
GITHUB_APP_NAME |
GitHub App slug |
GITHUB_APP_PRIVATE_KEY |
PEM private key (\n for newlines) |
You'll need a GitHub App with read/write access to repository contents.
essay/
├── cli/ # Go CLI
│ ├── cmd/ # auth, new, list, edit, publish
│ ├── internal/
│ │ ├── api/ # HTTP client to web backend
│ │ ├── config/ # ~/.config/essay/config.toml
│ │ └── github/ # device flow auth
│ ├── Makefile
│ └── .goreleaser.yml
│
├── web/ # Next.js app
│ ├── app/
│ │ ├── api/ # posts CRUD, CLI auth, GitHub webhooks
│ │ ├── [username]/ # public profile + post pages
│ │ └── dashboard/ # editor
│ ├── components/
│ ├── lib/ # db, github, auth helpers
│ └── prisma/
│
└── .github/workflows/
├── release.yml # goreleaser on v* tag → CLI binaries
└── release-please.yml # opens release PRs from conventional commits
CLI releases are fully automated:
- Commit to
mainusing conventional commits —feat:,fix:,chore:, etc. - release-please opens a versioned PR with a generated changelog
- Merge the PR → tag created → goreleaser builds + publishes binaries for all platforms
Web deploys automatically on every push to main via Vercel.
PRs welcome. For significant changes, open an issue first.
# run tests
cd cli && go test ./...
cd web && pnpm testMIT