No description
  • Go 67.4%
  • TypeScript 21.8%
  • HTML 5.5%
  • CSS 4.2%
  • Nix 1.1%
Find a file
Pi Agent 96320cf404
All checks were successful
CI / check (push) Successful in 48s
flake: refactor
2026-07-05 17:21:46 -04:00
.forgejo/workflows Use full URL for self-hosted action references 2026-06-03 21:32:41 +00:00
cmd feat: add proper versioning scheme 2026-05-27 09:29:30 -04:00
docs Add media uploads design document 2026-05-07 02:39:55 +00:00
internal feat: add proper versioning scheme 2026-05-27 09:29:30 -04:00
web feat: add help button 2026-06-03 20:16:46 -04:00
.envrc Nix build passing 2026-04-11 10:06:51 -04:00
.gitignore feat: ignore .cache 2026-05-15 18:46:55 -04:00
AGENTS.md feat: add proper versioning scheme 2026-05-27 09:29:30 -04:00
ALGORITHM.md Add structured event logging and migrate import paths 2026-05-06 18:09:29 +00:00
flake.lock flake: refactor 2026-07-05 17:21:46 -04:00
flake.nix flake: refactor 2026-07-05 17:21:46 -04:00
GET_TO_ANKI_TIER_1.md docs: mark Tier 1 complete, add Tier 2 implementation plan 2026-05-08 12:48:01 +00:00
GET_TO_TIER_2.md feat: restructure DOM with semantic HTML5 and ARIA for screen readers 2026-05-08 14:30:21 +00:00
GET_TO_TIER_3.md feat: multi-user Authelia auth, shared decks, owner-scoped data 2026-05-09 00:18:16 +00:00
go.mod Add structured event logging and migrate import paths 2026-05-06 18:09:29 +00:00
go.sum Nix build passing 2026-04-11 10:06:51 -04:00
LICENSE Nix build passing 2026-04-11 10:06:51 -04:00
README.adoc Add structured event logging and migrate import paths 2026-05-06 18:09:29 +00:00
README.md docs: fix shared deck auth explanation 2026-05-09 00:31:25 +00:00
VERSION feat: add proper versioning scheme 2026-05-27 09:29:30 -04:00

SpacedOut

Spaced repetition flash card app with a Go API server and a Vite multi-page frontend.

Build & Test

# Run all checks (frontend type checking + build, Go tests)
nix flake check

# Build all packages (server, cli, frontend)
nix build

# Run the server
./result/bin/server -db spacedout.db

The server embeds the frontend static assets and serves them alongside the API on a single port (:8080 by default). The frontend can also be built and served independently by any static file server (e.g. Caddy).

What nix flake check runs

Check What it does
frontend pnpm build (runs tsc with strict checks, then vite build)
goTests go test ./internal/... (all Go unit tests)

What nix build produces

Package Output
packages.default Server binary with embedded frontend
packages.server Same as default
packages.cli CLI binary
packages.frontend Static site (HTML + JS + CSS), can be served by Caddy or any static host

Development

# Enter dev shell (go, pnpm, nodejs, golangci-lint)
nix develop

# Backend
go build ./...
go test ./internal/...

# Frontend
cd web
pnpm install
pnpm dev          # dev server at :5173, proxies /api -> :8080
pnpm build        # tsc + vite build -> dist/

Multi-User Authentication

SpacedOut supports multi-user access via Authelia SSO. The Caddy reverse proxy at spaced.simple.fast uses forward_auth to validate every request with Authelia before forwarding to SpacedOut.

Authelia injects these trusted headers:

Header Example Source
Remote-User leoqi Authelia username
Remote-Groups admins Comma-separated groups
Remote-Email leoqi@simple.fast User email
Remote-Name Leo Qi Display name

All data (cards, decks, revlog, tags, config) is scoped per user via an owner column. The handler reads Remote-User and filters every query to that user's data.

Local Development

When running locally without Authelia, the Remote-User header is absent, so all queries use owner = "" (empty string). Since migration v7 defaults all owner columns to "", existing local data works as before — no Authelia dependency for development.

Migrating Existing Data

When deploying behind Authelia for the first time, existing data has owner = "" but authenticated users see owner = "leoqi". Reassign with:

spacedout migrate-owner leoqi

This updates all rows where owner = "" to the specified username.

Shared Decks

Users can share decks publicly. Shared deck endpoints are served without authentication via a Caddy path matcher that routes /s/* and /api/v1/share/* directly to the backend, bypassing Authelia's forward_auth.

Architecture

/
├── cmd/server/       API server (serves frontend + /api/v1)
├── cmd/spacedout/    CLI client
├── internal/
│   ├── frontend/     go:embed directory for built assets
│   ├── handler/      HTTP handlers
│   ├── model/        shared types
│   ├── observe/      structured event logging
│   ├── review/       study session logic
│   ├── scheduler/    FSRS scheduling algorithm
│   ├── store/        SQLite persistence
│   └── validate/     input validation
├── web/
│   ├── index.html          / (decks list)
│   ├── cards/index.html    /cards?decks=X (card listing)
│   ├── study/index.html    /study?deck=X (study session)
│   ├── add/index.html      /add?deck=X (add card)
│   └── src/
│       ├── api.ts          API client (API_VERSION constant)
│       ├── lib.ts          shared utilities (esc, truncation, deck helpers)
│       ├── math.ts         KaTeX rendering + CSS
│       ├── styles.css      shared CSS (dark theme, color-scheme: dark)
│       └── pages/          per-page TypeScript
└── flake.nix         build, test, dev shell

Frontend Pages

URL Page
/ Decks list
/cards All cards (no filter)
/cards?decks=math Cards in "math" deck
/cards?decks=math&decks=french Cards in "math" or "french"
/study?deck=math Flashcard study session
/study?deck=math&cram=true Cram mode (all cards, no limits)
/add?deck=math Add new card (pre-fills deck)
/analytics?deck=math Review heatmap, retention, forecast
/s/{token} Public shared deck viewer

The decks parameter is optional on /cards. If omitted, all cards are shown. The deck parameter on /add is optional and pre-fills the deck name.

Deck names support Unicode (Mandarin, etc.) and spaces. Examples: /cards?decks=数学, /cards?decks=Deck%20with%20spaces.

LaTeX is rendered with KaTeX using \(inline\) and \[block\] delimiters.

Card front/back text is truncated at 250 display columns in the card listing (CJK characters count as 2 columns).