Releases: OpenStudy-dev/OpenStudy
v0.6.0 — direct Postgres backend + 213-test backend suite
Internal hardening. No user-visible feature changes. The PostgREST service is gone — FastAPI now talks to Postgres directly via a psycopg async pool — and the project finally has a real backend test suite (213 tests against a real Postgres testcontainer with per-test transaction rollback). Plus a handful of correctness + security fixes surfaced by the post-migration audits.
Changed — architecture
- Dropped PostgREST. Backend services now use
psycopg[pool]async pool directly. Pool is opened/closed in the FastAPI lifespan; every service module migrated to the new helpers (db.fetch,db.fetchrow,db.fetchval,db.execute,db.db()). One fewer container, one fewer hop.POSTGREST_URL/POSTGREST_API_KEY/POSTGREST_AUTHenv vars removed; the pool readsDATABASE_URLdirectly. Stack is now three containers (postgres + openstudy + frontend), not four. - Auth code consumption is now atomic — single
DELETE … RETURNING *, closing the previous TOCTOU window. - Lecture-topics insertion is transactional —
add_lecture_topicswraps the optional lecture create + every topic insert in one psycopg transaction. /api/healthno longer blocks the event loop — storage stat moved off the request thread.- Rate limiter prefers
CF-Connecting-IPover the spoofableX-Forwarded-For.
Added — test infrastructure
- pytest + testcontainers-postgres with per-test transaction rollback (
force_rollback=True+ a_TxnPoolconnection pin) — clean DB every test without container churn. - 213 backend tests: ~145 service-layer + 60 MCP-tool tests across 11 files (one per entity family) + 5 helper unit tests + 3 multi-step end-to-end scenarios (full OAuth lifecycle, login rate-limit, TOTP enroll+login).
validated_colshelper — filters dict keys to Pydantic-declared schema fields before f-stringing into INSERT/UPDATE SQL. Defence in depth applied at all 16 patch/insert sites across 7 service files.
Added — OAuth
POST /oauth/revoke(RFC 7009) — public clients call this on logout. Advertised in/.well-known/oauth-authorization-server.
Fixed
update_settingsno longer NULLs valid timezone/locale when callers passNonefor unset parameters (exclude_none=True).update_settingsfallback insert usesON CONFLICT (id) DO UPDATE— concurrent first-callers no longer race the PK constraint into a 500./api/auth/totp/setupis now an upsert. On a fresh DB the previous bare UPDATE matched zero rows but returned 200 with a generated secret —/totp/enablethen 400'd.consume_auth_coderejects non-S256 PKCE unconditionally (OAuth 2.1 spec compliance).record_eventordering tie-breaker —clock_timestamp()+id DESCso events inserted in the same transaction order deterministically.storage._logsavepoint — swallowed FK violations no longer poison the outer request transaction.- Pool-level UUID loader — psycopg's
uuid.UUIDnow decodes tostrso Pydantic schemas don't need per-service conversions.
Removed
postgrest-pydependency, the PostgREST container, and the legacyapp.db.client()helper — all call sites migrated.
Migration notes (upgrading from v0.5.0)
- Pull, then
./deploy.sh— runs with--remove-orphansso the PostgREST container is cleaned up automatically. POSTGREST_URL/POSTGREST_API_KEY/POSTGREST_AUTHare no longer read; safe to remove from.env.- No schema changes.
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.
v0.3.0 — Five themes, full i18n, file manager
Big visual + localization release. Five dashboard themes, full English/German i18n, per-course schedule CRUD, a proper file manager in the Files tab, and a pile of phone-UX fixes. All backwards compatible — just run the one new migration on upgrade.
Added
- Five dashboard themes. Pick from Classic (the default — serif, airy), Terminal (mono, teal-on-black, hacker cockpit), Zine (pastel cream + hand-drawn stickers), Library (sepia, card-catalog aesthetic), or Swiss (12-col grid, red accent). Each is a full reskin — its own sidebar, CSS, and dashboard route, not just a palette. Picker in Settings → Theme.
- Full in-app i18n — English and German. Every route, form, toast, empty state, error message, and theme-specific prose runs through
i18next. Language picked explicitly in Profile → Language, persisted in localStorage, decoupled from the date-format locale. - Per-course schedule CRUD. Add / edit / delete weekly slots from the course-detail Schedule tab without leaving the page.
- File manager. Rename files and folders, recursive folder delete, create new folders, plus a folder picker on the course form so each course scopes its Files tab to a specific prefix in the bucket. New backend endpoints
/files/move,/files/folder, and a recursive listing helper. - Claude Design prompt template under
docs/claude-design-prompt.mdplus four worked-example outputs underdocs/examples/— the starting points for the Terminal / Zine / Library / Swiss themes.
Changed
- Phone UX pass. 16px form inputs (no more iOS zoom-on-focus), dvh for keyboard-aware layout, date-picker chrome contained inside its Field on iOS Safari, classic-theme weekly grid now renders the same multi-column time grid on phone (with horizontal scroll) instead of a stacked list.
- Course edit affordance moved from a hover overlay to an explicit Edit course button in the course-detail header. Notes and exam editing split into their own cards with their own edit buttons. "Scheduled" field on exams relabeled to "Exam date".
- Dashboard top strip on phone shows weekday / date / semester / week at a glance.
- Settings pickers (timezone, date format) auto-save on change; the semester-label text field gets an inline Save button while dirty. Success toasts are neutral now, not green.
- README hero replaced with a 2×2 still collage of the four paper themes plus a looping GIF of Terminal. Mirrored in the German section.
Upgrade from v0.2.0
git pull origin main
npx supabase db push # applies 20260421000001_theme.sql
cd web && pnpm install && pnpm buildThe migration adds app_settings.theme with default 'editorial', so existing rows land on the Classic theme until you pick something else.
Full changelog: v0.2.0...v0.3.0
v0.2.0 — English-canonical + Supabase CLI migrations
Rename pass. The project is now English-canonical from the database up through the MCP tool names, and migrations moved to supabase/migrations/ so the Supabase CLI tracks them properly.
If you're upgrading from v0.1.0, schema changes are required — git pull alone won't fix your DB. See the upgrade block below.
Breaking
- MCP tools renamed.
list_klausuren→list_exams,update_klausur→update_exam.upsert_schedule_slot→create_schedule_slot(pure create; useupdate_schedule_slotto patch).now_berlinremoved — usenow_here. Any cached tool lists in Claude.ai / Claude Code need re-fetching. - DB schema. Table
klausuren→exams. Columnscourses.klausur_weight/klausur_retries→exam_weight/exam_retries. - Enum values. Slot / lecture / topic / deliverable kinds normalised from German to English (
lecture|exercise|tutorial|lab,submission|project|lab|block, …). Legacy German values are still accepted at the API boundary and normalised — existing MCP integrations keep working. - Migration location.
db/migrations/→supabase/migrations/with timestamp-based filenames.
Added
- Single-file README with a same-page
<details name="lang">toggle — click 🇬🇧 English or 🇩🇪 Deutsch, the other collapses. - New migration
20260420000001_english_canonical_kinds.sqlthat normalises existing data on upgrade. - FastMCP server-level
instructions— domain mental model + tool-picking guidance injected oninitialize.
Changed
- Every MCP tool description rewritten with "when to use / when NOT to use" disambiguation. Tool count 46 → 44.
- UI: hardcoded German strings replaced with English everywhere (
Klausuren→Exams, slot-kind selects, etc.). INSTALL.md§4 now documentssupabase db pushas the primary flow with an upgrade path for existing DBs.
Upgrade from v0.1.0
git pull origin main
npx supabase link --project-ref YOUR-PROJECT-REF
# If you applied 0001–0004 via the SQL editor before, mark them applied first:
npx supabase migration repair --status applied 20260101000001 20260115000001 20260201000001 20260301000001
npx supabase db push # applies the English-canonical migrationThen rebuild the frontend (cd web && pnpm install && pnpm build) and redeploy.
Full changelog: https://github.com/AmmarSaleh50/study-dashboard/blob/main/CHANGELOG.md
v0.1.0 — initial public release
First public cut of study-dashboard. A self-hostable personal study dashboard with an MCP connector so Claude (claude.ai, iOS app, or Claude Code) can read and write your coursework.
What's in the box
Web app
- Dashboard — greeting, fall-behind banner, metric tiles, weekly time-rail grid, course cards, deadlines + tasks.
- Courses — create / edit / delete with per-course accent colors.
- Settings — profile (name, monogram, institution) and semester (label, start/end dates, timezone, locale).
- Dark-mode visual design (Fraunces serif + Inter Tight + JetBrains Mono, OKLCH palette, ink-dot signature motif).
- Mobile-first responsive layout.
MCP server
- Streamable HTTP transport at
/mcp, OAuth 2.1 gated. - ~45 tools — anything you do in the UI, Claude can do too.
read_course_filerenders PDFs to PNGs and streams them as vision input — Claude literally sees your slides.- Works with Claude.ai (browser + iOS), Claude Code CLI, ChatGPT connectors, anything else that speaks remote-HTTP MCP.
Stack
- Frontend: Vite + React 19 + TypeScript + Tailwind + shadcn/ui
- Backend: FastAPI (Python 3.12,
uv-managed) - Database: Supabase Postgres
- Hosting: Vercel (pre-configured; any Python host works)
Install
See INSTALL.md. ~15 min from empty machine to running dashboard.
Data model
Courses, schedule slots, lectures, study topics, deliverables, tasks, klausuren. Single-user per deploy by design.
Not yet in v0.1
- Light-mode pass (tokens support it; no one's tested it).
- Test suite — manual QA only right now.
- Non-Supabase database adapters (Postgres driver is Supabase-specific).
- i18n — slot kinds are German-labeled by default (
Vorlesung,Übung,Tutorium,Praktikum).
PRs welcome on any of the above — see CONTRIBUTING.md.