- Go 67.4%
- TypeScript 21.8%
- HTML 5.5%
- CSS 4.2%
- Nix 1.1%
|
|
||
|---|---|---|
| .forgejo/workflows | ||
| cmd | ||
| docs | ||
| internal | ||
| web | ||
| .envrc | ||
| .gitignore | ||
| AGENTS.md | ||
| ALGORITHM.md | ||
| flake.lock | ||
| flake.nix | ||
| GET_TO_ANKI_TIER_1.md | ||
| GET_TO_TIER_2.md | ||
| GET_TO_TIER_3.md | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| README.adoc | ||
| README.md | ||
| VERSION | ||
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).