v0.5.0 — Self-hosted Docker stack + landing page
Self-hosted by default. Big architectural shift: OpenStudy no longer
depends on Supabase or Vercel. The whole stack — Postgres, PostgREST,
FastAPI, and the React frontend — runs as four containers on any Docker
host, brought up with a single ./deploy.sh. Course files live on a
bind-mounted directory instead of object storage, indexed locally for
full-text search. On top of the architectural move, this release also
ships a public landing page, brand identity, TOTP 2FA, and a Telegram
bot integration.
Added — infrastructure
docker-compose.yml— four-service stack on an internal bridge
network:openstudy-postgres(Postgres 16-alpine),openstudy-postgrest
(PostgREST 12.2.3, JWT auth disabled, only reachable from the network),
openstudy(the FastAPI image built fromDockerfile), and
openstudy-frontend(the React SPA served by an in-container Caddy).
Only the frontend (127.0.0.1:8080) and FastAPI (127.0.0.1:8000) are
bound to the host; an outer reverse proxy (Caddy / nginx / Traefik)
forwards a single127.0.0.1:8080upstream.Dockerfile—python:3.12-slimbase, uv-managed deps, multi-layer
cache for fast rebuilds.web/Dockerfile— multi-stage build: Node 20 + pnpm builds the
Vite SPA, then acaddy:alpineimage serves it. The Caddyfile inside
the image does SPA fallback (try_files) plusreverse_proxy openstudy:8000for/api,/mcp,/oauthpaths../deploy.sh— single-command deploy with rollback. Pre-flight →
build both images → apply migrations → health-gate (GET /api/health
polled for 60s) → rollback to the previous image if health doesn't go
green. Flags:--skip-build,--no-rollback,--status,--help.- Migrations runner (
scripts/run_migrations.py) — idempotent,
transactional, sha256-tracked. State lives in a_migrationstable.
Files undermigrations/apply in filename order. - Initial schema as
migrations/00000000000000_baseline.sql—
canonical starting point for fresh deployments. Earlier development
history preserved undermigrations/_archive/for reference. - Filesystem storage layer (
app/services/storage.py) — files live
atSTUDY_ROOT(default/opt/courses); the storage service does
read / write / list / move / delete directly on disk. Browser file
serving via new/api/files/rawand/api/files/upload-target
endpoints (cookie-authenticated, same-origin). - Filesystem full-text index (
app/services/file_index.py,
scripts/index_files.py, baked into the baseline migration): walks
STUDY_ROOT, extracts text from PDFs / notebooks / markdown / typst,
upserts intofile_index. Search exposed asGET /api/files/search,
backed by thesearch_filesPostgres RPC for ranking + snippet
generation in one round-trip. /api/healthnow checks dependencies (DB SELECT + storage stat)
instead of returning a static{ok: true}./api/internal/*router (app/routers/internal.py) —
bearer-gated (X-Internal-Secret) endpoints for cron jobs to trigger
reindex, plus a Telegram-bot webhook (authed via Telegram's own
X-Telegram-Bot-Api-Secret-Tokenheader) exposing/sync,/status,
/helpto the operator's allowlisted chat.
Added — frontend & brand
- Brand assets —
web/public/brand/{mark,wordmark}/{on-light,on-dark}.svg,
rendered via the new<Wordmark>React component
(web/src/components/brand/wordmark.tsx) and embedded in the README
header. - Landing page at
/(web/src/routes/landing.tsx+
web/src/styles/landing.css): hero with auto-rotating five-theme
carousel, animated MCP / Day-0 demo, real Claude Desktop screenshots,
self-host terminal block, GitHub-stars CTA, floating navbar that
hides on scroll-down. All CTAs link to the GitHub repo — no waitlist
or signup. VITE_SHOW_LANDINGenv flag (defaultfalse) — whentrue,/
renders the landing page; whenfalse,/redirects straight to the
app (/appif signed in,/loginotherwise). Self-hosters typically
leave it off.scripts/build-seo.mjs— Vite prebuild step that regenerates
robots.txt,sitemap.xml, andmanifest.webmanifestfrom
VITE_SITE_URL/VITE_SITE_NAME. Forks deploying to a custom domain
get correct canonical URLs and PWA metadata without code edits.- SEO + PWA assets —
web/public/og-card.png,apple-touch-icon.png,
icon-192/256/512.png,security.txt,manifest.webmanifest. - TOTP / 2FA for the dashboard login
(web/src/components/settings/totp-card.tsx, baked into the baseline
migration). Setup-key + QR + recovery-code flow inside Settings. - Multi-language
<title>and<html lang>via
web/src/lib/document-head.ts— switches between EN / DE based on
the active i18n locale.
Changed
POSTGREST_URL/POSTGREST_API_KEYenv vars replace
SUPABASE_URL/SUPABASE_SERVICE_KEY. Breaking change for anyone
upgrading from v0.3.x — see migration notes below.POSTGREST_AUTHflag — set tofalseto skip Bearer auth headers
when targeting a self-hosted PostgREST that has JWT validation off.app/db.py— function renamedsupabase()→client(). All
service files migrated tofrom app.db import client./api/internal/sync— runs reindexing in a FastAPI background
task instead of spawning subprocesses. Themodequery parameter is
still accepted (and echoed back) for caller compatibility, but no
longer affects behaviour.- README, INSTALL.md, CONTRIBUTING.md,
.env.example
all rewritten around the docker-compose deploy. README header shows
the OpenStudy wordmark with auto light / dark variants instead of a
plain heading; database badge updated from "Supabase Postgres" to
"Postgres 16". PUBLIC_SITE_URLis the single source of truth for the domain
baked into canonical / OG / sitemap / manifest tags. Previous default
openstudy.devremoved; default is nowhttp://localhost:8080so
forks don't accidentally ship with someone else's domain.N8N_MOODLE_WEBHOOK_URLhas no default any more — endpoints that
use it 503 with a helpful message when unset, instead of trying to
hit a hardcoded host.
Removed
- Vercel artefacts —
vercel.json, theapi/index.pyshim, related
.vercel/config. Vercel was retired as a host; the "build dist +
rsync to a static web server" deploy path is gone too. - Supabase-specific layout — top-level
supabase/folder. Migrations
live undermigrations/now. - Bucket-sync scripts —
force_push_to_bucket.py,sync.py,
openstudy.py, the bidirectional CONFLICT-DEL-REMOTE state machine.
With local filesystem storage there's nothing to mirror to a separate
object store. Moved toscripts/_deprecated/for reference. TRADEMARK.md— the project ships under MIT only, with no
separate trademark policy. Self-host rebranding guidance now lives
in CONTRIBUTING.md (VITE_SITE_URL/VITE_SITE_NAME+ brand assets).
Migration notes (upgrading from v0.3.x)
This is a breaking release. If you're moving an existing OpenStudy
install over from Supabase + Vercel:
pg_dumpyour Supabase database and restore it into the new local
Postgres before first running./deploy.shagainst real users — see
INSTALL.md §4.- Rename
SUPABASE_URL→POSTGREST_URLandSUPABASE_SERVICE_KEY→
POSTGREST_API_KEYin your.env. Add a new.env.dockernext to
it withPOSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DBfor the
database container. - Move your course files into the path you'll mount as
STUDY_ROOTin
the compose file (default/opt/courses). - Make sure the
courses.folder_namecolumn is populated for every
course — it's now the source of truth that/api/files/lecture-materials
and the file browser use to map a course code to its on-disk folder
(replaces the previously hardcoded mapping). - Drop your Vercel deployment once the new docker host is healthy.
Point your domain at the new outer reverse proxy.