Incremental reading with AI-generated flashcards. Import PDFs, EPUBs, and web articles, extract key concepts into reviewable cards, and sync with Anki.
Try it at freememo.net — no setup required. Or self-host using the instructions below.
Built with Hyperfiddle Electric v3 (Clojure/ClojureScript) — a reactive framework where client and server code coexist in the same .cljc files with automatic WebSocket sync.
- Import — PDFs, EPUBs, web articles (paste or Wikipedia lookup)
- AI flashcard generation — OpenAI extracts key concepts into basic and cloze cards
- OCR — OpenAI Vision converts PDF pages to editable semantic HTML
- Rich text editor — Quill-based editor with highlighting and selection-based card generation
- Anki sync — Push/pull cards directly to your Anki collection via AnkiConnect
- Zotero import — Pull PDFs (with citation metadata) straight from your local Zotero library via the FreeMemo for Zotero plugin
- PDF viewer — In-browser rendering via PDF.js with zoom and navigation
- Subset review — Review a chosen subset of cards from any topic
- Search — Full-text search across topics and cards
- Library — Browse imported documents
- Contents — Hierarchical knowledge tree with virtual scrolling
- Status — Progress overview across all documents
- Java 17+
- Clojure CLI (install guide)
- Docker (for PostgreSQL)
- Node.js (for shadow-cljs)
# Start PostgreSQL
docker compose up -d
# Start dev server (hot reload, port 8080)
clj -M:dev -m devOpen http://localhost:8080.
Navigate to Settings in the UI and enter your OpenAI API key. All settings are persisted per-user in the database.
Note: OCR and flashcard generation use a per-user OpenAI API key (BYOK) by default. Self-host operators MAY set the
OPENAI_DEMO_KEYenvironment variable as a server-wide fallback; it is used only for users who have not set their own key in Settings.
Sign-in uses Google OAuth. Self-host requires configuring GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET / GOOGLE_REDIRECT_URI (or dropping a resources/google_client.json from Google Cloud Console). Without these, /auth/google returns 503.
Zotero import is gated by a small Zotero plugin (the FreeMemo for Zotero add-on under freememo-zotero-plugin/). The plugin runs inside the user's Zotero, exposes a CORS-permissive HTTP API under /freememo/* on Zotero's loopback server (port 23119), and streams attachment bytes inline. The FreeMemo browser tab talks to it directly — Zotero data never travels through the FreeMemo server.
To enable end-to-end:
- Get the
.xpi. Either:- Download the pre-built
.xpicommitted in this repo atfreememo-zotero-plugin/dist/freememo-zotero-plugin-0.1.0.xpi— recommended for most users; or - Build from source:
cd freememo-zotero-plugin && ./build.sh(producesdist/freememo-zotero-plugin-<version>.xpi).
- Download the pre-built
- Install in Zotero: Tools → Plugins → gear icon → "Install Plugin From File…", pick the
.xpi. Restart Zotero. - Zotero → Settings → Advanced → enable "Allow other applications on this computer to communicate with Zotero".
- In FreeMemo: Settings → Zotero → toggle Enable Zotero import on. Click Test Connection to verify.
Why a plugin: Zotero's built-in API sends no CORS headers, returns file:// redirects for binary attachments (unreachable from web JS), and silently cancels any "browser-shaped" request as a CSRF mitigation. A Zotero plugin is the smallest layer that solves all three. See freememo-zotero-plugin/README.md for the full rationale, endpoint contracts, and dev-proxy workflow.
| Layer | Technology |
|---|---|
| Framework | Electric v3 — reactive full-stack Clojure |
| Database | PostgreSQL 16 (next.jdbc + HikariCP + HoneySQL) |
| Build | deps.edn + shadow-cljs |
| PDF rendering | PDF.js 3.11.174 (CDN) |
| Rich text | Quill 2.0.3 (CDN) |
| Logging | Telemere (structured, CLJ + CLJS) |
| AI | OpenAI GPT-5.1 (card generation) + Vision (OCR) |
PostgreSQL 16 via docker-compose.yml. Default connection: cardmaker:dev@localhost:5432/cardmaker.
Schema auto-creates on first startup (no migration framework). Configure via environment variables — see below.
| Variable | Default | Notes |
|---|---|---|
DB_HOST |
localhost |
|
DB_PORT |
5432 |
|
DB_NAME |
cardmaker |
|
DB_USER |
cardmaker |
|
DB_PASSWORD |
dev |
Override in production |
ENC_KEY_SECRET |
(none) | Session-cookie key derivation. Required in production. Generate: openssl rand -hex 32 |
LOG_LEVEL |
info (prod) / debug (dev) |
trace, debug, info, warn, error, fatal |
PORT |
8080 |
HTTP port |
STORAGE_QUOTA_BYTES |
1073741824 (1 GB) |
Total per-user storage. 0 = unlimited. |
STORAGE_PER_FILE_MAX_BYTES |
104857600 (100 MB) |
Per-file upload cap. 0 = unlimited. Also drives Jetty body + WebSocket size limits. |
APP_BASE_URL |
https://freememo.net |
Public base URL embedded in Anki card source anchors. Set to your domain when self-hosting. |
GOOGLE_CLIENT_ID |
(none) | Google OAuth client ID. Required for sign-in. |
GOOGLE_CLIENT_SECRET |
(none) | Google OAuth client secret. |
GOOGLE_REDIRECT_URI |
http://localhost:8080/auth/google/callback |
Must match the URI registered in Google Cloud Console. |
OPENAI_DEMO_KEY |
(none) | Server-wide fallback OpenAI API key. Used only when a user has not set their own key in Settings. |
Alternative to the three GOOGLE_* envs: drop the OAuth client JSON downloaded from Google Cloud Console at resources/google_client.json (gitignored).
Self-host requires Google OAuth. There is no username/password login route.
# Build client
clj -X:build:prod build-client
# Run production server
clj -M:prod -m prod
# Or build an uberjar
clj -J-Xss4m -X:build:prod uberjar :build/jar-name '"app.jar"'
java -jar target/app.jarOne-command bundle (app + Postgres):
cp .env.example .env # then edit DB_PASSWORD, ENC_KEY_SECRET, GOOGLE_* (optional)
docker compose -f self-host.yml up -dStorage caps default to unlimited in the bundle. Set STORAGE_QUOTA_BYTES / STORAGE_PER_FILE_MAX_BYTES in .env to enforce them.
Or build and run the app image manually:
docker build -t freememo:latest .
docker run -d --name freememo -p 8080:8080 \
-e DB_HOST=your-db-host \
-e DB_PASSWORD=your-password \
-e ENC_KEY_SECRET=$(openssl rand -hex 32) \
freememo:latestThe app runs on port 8080. For HTTPS, put it behind nginx or Caddy. WebSocket support is required — Electric v3 uses persistent WebSocket connections.
nginx essentials:
location / {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
client_max_body_size 100M; # must be ≥ STORAGE_PER_FILE_MAX_BYTES (or 0/large when unlimited)
}Caddy (automatic HTTPS):
your-domain.com {
reverse_proxy localhost:8080
}
Backup the database:
docker compose -f self-host.yml exec db pg_dump -U cardmaker cardmaker > backup.sqlRestore:
cat backup.sql | docker compose -f self-host.yml exec -T db psql -U cardmaker cardmakerA remote-pull variant is in scripts/backup.sh.
Electric v3 is free for bootstrappers and non-commercial use, and available commercially under a business source license. See Electric v3 license.