Skip to content

Repository files navigation

btsv

README current as of 2f3dec8 (2026-08-21).

A web-based markdown+ document editor. Write posts in a SvelteKit PWA, persist them to a git repo via isomorphic-git, and publish through Astro (or Hugo, etc.) deployed to Netlify or Cloudflare.

Features

  • Local-first — posts live in IndexedDB; sync to the git repo happens in the background
  • Editor — markdown+ with autosave, offline editing, and tags autocomplete
  • Post list — sort by edited / created / published date; filter by tag, draft, or page
  • Publishing model — drafts excluded from production builds; page posts excluded from listings/RSS
  • PWA — installable, works offline

Architecture

┌──────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   apps/web   │────▶│  apps/proxy │────▶│   apps/api  │     │  Git repo   │
│  (SvelteKit) │     │   (Go CORS  │     │    (Go)     │     │ (content +  │
│   Editor SPA │     │    proxy)   │     │  Auth +     │     │  Astro SSG) │
│              │◀────│             │◀────│  secrets    │     │             │
└──────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
     local-first                               │                     ▲
     IndexedDB +                               │                     │
     isomorphic-git ─ ─ ─ ─ ─ ─ ─ git push ─ ─ ┘  with server- ─ ─ ─ ┘
                                                  stored PAT
Layer Component Description
Frontend apps/web SvelteKit 5, adapter-static SPA, Svelte 5 runes, local-first PWA
Backend apps/api Go + chi, SQLite, username/password auth, encrypted token storage
Proxy apps/proxy Go CORS proxy for Git remote API calls
Storage Git repos Content repos per user/project; isomorphic-git pushes from the browser
Publish builder-templates/ SSG templates (submodules) — users fork and connect to a provider

Getting started

Prerequisites

  • Node ≥ 24, pnpm ≥ 10
  • Go ≥ 1.25
  • golangci-lint (for linting the API)

Clone

This repo uses git submodules for the builder templates. Use a recursive clone:

git clone --recurse-submodules https://github.com/btsv-space/btsv.git

If you've already cloned without --recurse-submodules:

git submodule update --init --recursive

Local Development

# Start all services (proxy, API, web)
make dev

# Or individually
make dev-web   # http://localhost:5173
make dev-api   # http://localhost:8080
make dev-proxy # http://localhost:9999

Mobile / network testing (HTTPS)

crypto.subtle (used for isomorphic-git) requires a secure context. Localhost works in HTTP, but testing from other devices on your LAN needs HTTPS.

First install mkcert and generate certs:

brew install mkcert   # macOS
mkcert -install
mkdir -p .certs
mkcert -cert-file .certs/dev.pem -key-file .certs/dev-key.pem \
  192.168.0.63 localhost 127.0.0.1

Replace 192.168.0.63 with your machine's actual LAN IP, or add extra IPs.

Then start everything with HTTPS:

make dev-host

The dev-host target detects your LAN IP automatically, sets ALLOW_ORIGIN=*, and passes TLS certs to all three services (Vite, API, proxy):

https://192.168.0.63:5173   Editor (Vite + SvelteKit)
https://192.168.0.63:8080   API (Go + chi)
https://192.168.0.63:9999   CORS proxy (Go)

Your browser will trust the certs automatically (mkcert root CA). On a phone:

  • iOS: AirDrop ~/Library/Application\ Support/mkcert/rootCA.pem to the phone → Settings → General → Profiles → install → Settings → General → About → Certificate Trust Settings → enable.
  • Android: Copy rootCA.pem to the phone → Settings → Security → Install from storage.

Without the CA, browsers show a warning — tap "Proceed anyway" (works fine).

Build

make build
# Frontend → apps/web/build/
# API      → apps/api/bin/server

Lint

make lint

Test

make test           # Unit tests (web, api, proxy)
make test-e2e-web   # Playwright E2E — boots API + vite itself (uses .env.e2e)

Deployment (Docker)

The three services are dockerized for production deployment:

Service Image Port Domain
Web nginx (static SPA) 8103 app.example.com
API Go binary on alpine 8101 api.example.com
Proxy Go binary on alpine 8102 proxy.example.com

Prerequisites

  • Docker & Docker Compose

Build & run

docker compose --env-file .env.production up --build -d

Environment

Production domain URLs (VITE_API_URL, VITE_PROXY_URL, ALLOW_ORIGIN) are set as Docker build args and environment variables in docker-compose.yml. Update .env.production if you run make start locally with production builds instead of Docker.

Variable Set in Purpose
VITE_API_URL web build arg API base URL baked into the SPA bundle
VITE_PROXY_URL web build arg Git CORS proxy URL baked into the SPA bundle
ALLOW_ORIGIN api, proxy env vars CORS origin header (the web app's domain)
PORT api & proxy env vars Internal listen port
DATA_DIR api env var SQLite database path (persisted via named volume)
COOKIE_DOMAIN api env var Session cookie domain; when set, also enables the Secure flag. Use .example.com in production; leave empty in dev

Submodules

Builder templates live as git submodules under builder-templates/. Each is an independent repo that users fork to create their own blog site. We develop them alongside the editor to keep the content contract in sync.

Pulling the latest template

# Pull the latest commit from the submodule's main branch
git submodule update --remote builder-templates/btsv-template-astro

# Commit the updated submodule pointer in the main repo
git add builder-templates/btsv-template-astro
git commit -m "Update btsv-template-astro submodule"

Making changes to a template

cd builder-templates/btsv-template-astro

# Work on the template, commit, push
git checkout main
# ... make changes ...
git add -A
git commit -m "Description of changes"
git push origin main

# Back in the main repo, record the new submodule pointer
cd ../..
git add builder-templates/btsv-template-astro
git commit -m "Update btsv-template-astro: description"

Content contract

The contract/ directory is the single source of truth between the editor and all builder templates. It contains a JSON Schema that defines the frontmatter shape.

Core fields

Field Type Required Description
title string yes Post title
dateCreated datetime yes Original creation date
dateUpdated datetime yes Last modified date
datePublished datetime no Publication date (set when a draft is first published)
description string no SEO/social preview
tags string[] no Tag list
draft boolean no Exclude from production builds
id string no Internal identifier (auto-generated)
slug string no Custom URL slug
page boolean no Standalone page, excluded from listings/RSS

Datetimes are ISO 8601 UTC (e.g. 2026-08-19T14:30:22Z); legacy day-precision values (YYYY-MM-DD) remain valid.

Custom fields (escape hatch)

The schema is strict (additionalProperties: false), but users can add any extra fields to their frontmatter — the editor's parser captures them into extra, passes them through untouched, and builder templates can read them. Core fields get dedicated editor UI; custom fields don't (yet).

Markdown+

Posts use GitHub-flavored Markdown via MDX, plus:

  • <Callout>, <Figure> — MDX components shipped by builder templates
  • @@ ... @@@ — editor-only comment blocks stripped at build time

See contract/README.md for the full specification.

API contract

The backend exposes a REST API consumed by the frontend (apps/web/src/lib/api.ts). The API base URL defaults to http://localhost:8080/api and can be overridden with VITE_API_URL.

Auth

Method Path Auth Description
POST /api/auth/register { username, password, encryptedDek, kekSalt } → user
POST /api/auth/login { username, password }{ user, encryptedDek, kekSalt }, sets session cookie
POST /api/auth/logout Clears session cookie
GET /api/auth/me session { id, username, encryptedDek, kekSalt } or null
POST /api/auth/change-password session { oldPassword, newPassword, encryptedDek, kekSalt } → 204

Projects

Method Path Auth Description
GET /api/projects session [{ id, name, repoUrl, createdAt }]
POST /api/projects session { name, repoUrl } → project
GET /api/projects/:id/secret session { ciphertext, iv } (client-encrypted git token)
POST /api/projects/:id/secret session { ciphertext, iv } → 204

User preferences

Method Path Auth Description
GET /api/user/preferences session → preferences JSON
PATCH /api/user/preferences session Partial update → preferences JSON

Health

Method Path Description
GET /health { "status": "ok" }

Security

  • Git tokens are encrypted client-side (AES-GCM) with a per-user data key (DEK) before being sent to the API — the server only ever stores ciphertext
  • The DEK is wrapped by a key derived from the user's password (PBKDF2) and stored as encryptedDek + kekSalt; changing the password re-wraps the DEK
  • Plaintext tokens are used in-memory by the frontend and never written to localStorage or IndexedDB
  • Session cookies are HttpOnly, SameSite=Strict, random 256-bit tokens with 14-day expiry

Project structure

btsv/
├── apps/
│   ├── web/                              SvelteKit 5 SPA (adapter-static)
│   ├── api/                              Go + chi REST server
│   ├── proxy/                            Go CORS proxy (GitHub / GitLab)

├── builder-templates/
│   ├── btsv-template-astro/              Astro blog template (git submodule)
│   └── btsv-template-hugo/               Hugo blog template (git submodule)
├── contract/
│   ├── frontmatter.schema.json           Canonical JSON Schema
│   └── README.md                         Contract specification
├── Makefile                              Top-level dev/build/lint commands
├── .editorconfig
└── .gitignore

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages