Your personal, self-hosted recipe collection.
Forkit is a self-hosted recipe accumulator that lets you collect recipes from anywhere on the web and discover new ones from hundreds of recipe sites โ all from a single, local-first interface. Paste any recipe URL, Forkit extracts clean, structured recipe data and stores it in your personal library. Search, filter, tag, and build your own cookbook.
graph TD
FE["<b>SvelteKit 5 Frontend</b><br/>Recipe browser ยท Library ยท Discovery ยท Settings"]
BE["<b>FastAPI Backend</b><br/>Python 3.12 ยท async ยท rate-limited"]
PARSER["<b>Recipe Parser</b><br/>recipe-scrapers ยท JSON-LD ยท HTML fallback"]
DISC["<b>Discovery Service</b><br/>Adapter registry ยท federated search"]
A1["html_scrape"]
A2["feed"]
A3["google_cse"]
A4["dataset"]
MAIN[("SQLite<br/><i>forkit.db</i><br/>recipes ยท feeds ยท sites")]
FTS[("SQLite FTS5<br/><i>forkit_dataset.db</i><br/>230K+ community recipes")]
FS[("File Storage<br/><i>/data/images</i><br/>hero images")]
FE -->|HTTP REST| BE
BE --> PARSER
BE --> DISC
DISC --> A1 & A2 & A3 & A4
A4 --> FTS
PARSER --> MAIN
PARSER --> FS
BE --> MAIN
flowchart TD
U([User pastes URL]) --> FETCH[Fetch page]
FETCH --> RS{recipe-scrapers\nmatch?}
RS -->|Yes| EXT[Structured extraction]
RS -->|No| JL{JSON-LD /\nmicrodata?}
JL -->|Yes| EXT
JL -->|No| HTML[HTML fallback parser]
HTML --> EXT
EXT --> SAVE[(SQLite โ forkit.db)]
EXT --> IMG[Download hero image]
IMG --> FS[(File storage)]
flowchart LR
Q([Search query]) --> REG[Adapter Registry]
REG --> S1[html_scrape\nadapter]
REG --> S2[feed\nadapter]
REG --> S3[google_cse\nadapter]
REG --> S4[dataset\nadapter]
S1 & S2 & S3 & S4 --> MERGE[Merge results]
MERGE --> OUT([RecipeStub list])
S4 -. queries .-> FTS[(FTS5\nforkit_dataset.db)]
A static single-page app built with Svelte 5 runes. No server-side rendering. Communicates with the backend via REST API. Features recipe search, library management, and discovery interface. Service worker support for offline-ready PWA behavior.
Python 3.12 async API server. Routes all user requests and discovery queries to the appropriate service. Enforces rate limiting (10 req/min for most endpoints, 5 req/min for heavy operations like site testing). Structured logging with request context for debugging.
Two SQLite databases:
forkit.db): Recipes, ingredients, tags, cook history, discovery sites, subscribed feeds, and search cachesforkit_dataset.db): Food.com recipe corpus (~230K recipes) indexed with SQLite FTS5 for full-text search. Imported on-demand via CLI.Abstracts multiple recipe sources behind a unified interface. Four adapter types:
uv for local developmentClone the repository and start the stack:
git clone https://github.com/10Legs/forkit.git
cd forkit
docker compose up -d
Open your browser to http://localhost:8080 (or the port configured in your environment).
Backend dev server (auto-reload):
cd backend
uvicorn forkit.main:app --reload --host 0.0.0.0 --port 8001
Frontend dev server:
cd frontend
npm run dev
# Opens http://localhost:5173
Run tests:
make test
Forkit uses recipe-scrapers (supports 100+ sites) and falls back to structured data extraction (JSON-LD, microdata) and HTML parsing if needed.
Use Discovery to search recipes without saving them first:
By default, Forkit searches:
Settings โ Discovery โ Add Site
Choose how Forkit should search the site:
epicurious.com/recipes/)seriouseats.com/feed)Forkit ships with built-in discovery sites, but you can optionally import the Food.com dataset for local, full-text search across 230K+ recipes.
Download the dataset from Kaggle:
RAW_recipes.csv/tmp/RAW_recipes.csv)If using Docker:
docker compose exec web forkit dataset import foodcom \
--data-path /tmp/RAW_recipes.csv \
--accept-license
If running locally:
forkit dataset import foodcom \
--data-path /tmp/RAW_recipes.csv \
--accept-license
The importer will:
forkit_dataset.db (~1.5GB on disk)Check import progress:
docker compose exec web forkit dataset stats
Note: Dataset is CC-BY-SA 3.0. You must accept the license to import.
Set environment variables in docker-compose.yml or .env:
| Variable | Default | Description |
|---|---|---|
PORT |
8080 |
Port to expose the app (maps to backend 8000) |
FORKIT_DB_PATH |
/data/forkit.db |
Path to main SQLite database |
FORKIT_IMAGE_DIR |
/data/images |
Path to store downloaded recipe images |
FORKIT_CACHE_DIR |
/data/cache |
Path to cache directory (includes dataset DB) |
FORKIT_LOG_LEVEL |
info |
Logging level: debug, info, warning, error |
FORKIT_AUTH_MODE |
none |
Auth strategy: none or acknowledge_wan_exposed (security) |
HERMITHOST_PORT_MODE |
lan |
HermitHost integration: lan or other (security gate) |
For WAN exposure, set auth mode:
docker compose up -e FORKIT_AUTH_MODE=acknowledge_wan_exposed -d
| Layer | Technology | Notes |
|---|---|---|
| Frontend | SvelteKit 5, Svelte 5 (runes) | Static SPA, client-side routing |
| Backend | FastAPI 0.115+, Python 3.12 | Async REST API, structured logging |
| Database | SQLite 3 + FTS5 | Main DB + dataset index, no server needed |
| ORM | SQLAlchemy 2.0 | Async sessions, migrations via Alembic |
| Recipe Parsing | recipe-scrapers 15+, BeautifulSoup, extruct | Covers 100+ recipe sites + fallbacks |
| Feed Parsing | feedparser 6.0, defusedxml | Safe RSS/Atom feed parsing |
| HTTP Client | httpx 0.27 | Async, rate-limited via slowapi |
| Structured Logging | structlog | JSON logs with request context |
| Rate Limiting | slowapi | Per-IP limits (10 req/min default) |
| Containerization | Docker Compose | Single-file deployment |
List all recipes in the personal library.
Query parameters:
skip (int): Pagination offset (default 0)limit (int): Page size (default 20)search (str): Full-text search querycuisine (str): Filter by cuisinetag (str): Filter by tag IDResponse: { recipes: Recipe[], total: int }
Get full recipe details.
Response: Recipe object with all fields.
Add a recipe from a URL.
Request body:
{
"url": "https://example.com/recipe/chocolate-cake"
}
Response:
{
"id": "uuid",
"title": "Chocolate Cake",
"state": "ingested",
"hero_image_path": "/images/uuid.jpg"
}
Rate limited to 10 requests per minute per IP.
Search recipes across configured discovery sites.
Query parameters:
q (str): Search query (required)timeout_sec (int): Timeout per site (default 10)Response: { results: RecipeStub[], errors: { site_name: error_msg } }
List all configured discovery sites.
Response: { sites: DiscoverySite[] }
Sensitive fields (API keys, tokens) are redacted.
Test a discovery site configuration.
Response: Sample results or error details.
Rate limited to 5 requests per minute per IP.
List recently polled feed items.
Response: { items: FeedItem[] }
Manually poll all subscribed feeds.
Response: { polled_count: int, new_items: int }
Rate limited to 5 requests per minute per IP.
Service health check.
Response:
{
"status": "ok",
"version": "0.1.0",
"db_ok": true
}
forkit/
โโโ backend/
โ โโโ src/forkit/
โ โ โโโ main.py โ FastAPI app entry point
โ โ โโโ api/ โ HTTP endpoints
โ โ โโโ parser/ โ Recipe URL parsing & fetching
โ โ โโโ discovery/ โ Multi-source search
โ โ โโโ dataset/ โ Food.com dataset import & search
โ โ โโโ models/ โ SQLAlchemy ORM models
โ โ โโโ db.py โ SQLAlchemy session management
โ โ โโโ config.py โ Settings from environment
โ โ โโโ cli.py โ forkit CLI (dataset import)
โ โโโ pyproject.toml โ Dependencies & metadata
โ โโโ Dockerfile โ Container build
โ โโโ tests/ โ Pytest test suite
โโโ frontend/
โ โโโ src/routes/ โ SvelteKit page components
โ โโโ src/lib/ โ Reusable components & utilities
โ โโโ svelte.config.js โ SvelteKit config (SPA mode)
โ โโโ package.json โ Dependencies
โ โโโ vite.config.js โ Vite config
โโโ docker-compose.yml โ Multi-service deployment
โโโ Makefile โ Common commands
โโโ README.md โ This file
make test
Runs pytest (backend) and npm test (frontend).
Backend (Python):
cd backend
ruff check .
mypy .
Frontend (JavaScript/TypeScript):
cd frontend
npm run lint
make build
Creates an optimized Docker image. Builds frontend inside the container.
Forkit uses Alembic for schema migrations.
Create a new migration:
docker compose exec web alembic revision --autogenerate -m "description"
Apply pending migrations:
make migrate
Symptom: Container logs show forkit refuses to start: HERMITHOST_PORT_MODE is not 'lan'
Solution: If exposing Forkit on the WAN (not localhost), set:
docker compose up -e FORKIT_AUTH_MODE=acknowledge_wan_exposed -d
This is a security guard to prevent accidental public exposure without auth.
Symptom: "Failed to extract recipe" error
Possible causes:
Solutions:
Symptom: Discovery search returns { errors: { site_name: "timeout" } }
Cause: A discovery site is slow to respond or unreachable
Solutions:
timeout_sec parameter (API default is 10s)Symptom: forkit dataset import command fails with parsing errors
Possible causes:
Solutions:
RAW_recipes.csv from Kaggledf -h /datadocker compose logs webSymptom: Recipe cards show broken image icons
Cause: Image download failed during ingestion, or image directory not mounted
Solutions:
forkit-data:/datadocker compose exec web ls -la /data/imagesContributions are welcome! Whether it's a bug fix, new discovery site adapter, or UX improvement, please open an issue or pull request.
Forkit is provided as-is. See the LICENSE file for details.
Recipe data from Food.com is CC-BY-SA 3.0. Respect those terms when using the dataset.