The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Cellarion listing page.
Cellarion is a hosted wine cellar app — a ready-to-use online service at cellarion.app. Create a free account and start tracking your bottles, organizing them into cellars and racks, searching a shared wine registry, getting sommelier-curated drink-window recommendations, and chatting with an AI sommelier about your collection. No installation, no server, no setup — just sign up and go. Every feature is free, forever.
Just want to use Cellarion? Go to cellarion.app and sign up. You do not need to clone this repository, run Docker, or host anything yourself. There's also a public demo account (Try the demo on the landing page) and an Android app on Google Play.
Cellarion is also open-source (AGPL-3.0), so if you'd prefer to run your own private instance, you can self-host it. The rest of this README covers self-hosting — see Quick Start. Most people should just use the hosted service at cellarion.app.
Cellarion is live and publicly available at:
This is the primary way to use Cellarion. Create an account and start using the full service today — every feature is free, forever. No credit card, no trial clock, no paywalled features, nothing to install or maintain. If you want to chip in, optional Supporter, Patron and Benefactor tiers (monthly or yearly) and GitHub Sponsors fund development — they unlock nothing extra, just our thanks.
Your cellar
The shared registry
AI
Community & sharing
Platform
/api/mcp) with OAuth, for AI assistants/api/ to the backend (internal)--profile analytics)This section is only for people who want to run their own private instance. If you just want to use Cellarion, head to cellarion.app instead — no setup required.
The app is routed through Traefik, so create the external web Docker network before the first up (compose declares it external — the first command fails otherwise):
| URL | Description |
|---|---|
| http://localhost | Frontend (React SPA) — served via Traefik |
| http://localhost/api/health | Backend health check |
After the containers are running:
This creates demo accounts plus a starter taxonomy, wine registry entries, and a demo cellar with sample bottles:
| Account | Password | Role | |
|---|---|---|---|
| Admin | admin@cellarion.app | Admin1234!demo | admin |
| Demo user | user@cellarion.app | User1234!demo | user |
These are local development credentials. Change them before deploying anywhere public.
The tree below shows the shape of the codebase — the highlights, not every file. The backend has 57 Mongoose schemas and ~65 route modules (44 top-level + admin/ + somm/), plus the MCP server.
All external traffic enters through Traefik (runs on the shared web Docker network, external to this Compose file). All services inside this Compose file are internal only.
| Service | Host port | Description |
|---|---|---|
| Traefik | 80 | External reverse proxy (external) |
| nginx | internal | Serves React SPA + proxies /api/ |
| Backend | internal | Express REST API (port 5000) |
| MongoDB | internal | Database (port 27017) |
| Meilisearch | internal | Fuzzy search engine (port 7700) |
| Qdrant | internal | Vector database (port 6333) |
| rembg | internal | Background removal (port 5000) |
| Umami (+db) | internal | Optional analytics — --profile analytics |
Cellarion is designed to sit behind a Traefik reverse proxy on a shared Docker network called web. Traefik handles incoming HTTP on port 80 (SSL termination is handled upstream by Cloudflare or similar).
Requirements:
webweb network must exist before starting Cellarion: docker network create webThe frontend service declares the following Traefik labels in docker-compose.yml:
The upstream port is 8080 (not 80): the frontend image is built on
nginxinc/nginx-unprivileged, whose non-root nginx cannot bind ports below 1024.
Update the Host(...) rule to match your own domain.
Cellarion ships a built-in Model Context Protocol server, so AI assistants can read and manage your cellar conversationally — "what should I open tonight?", "add these six bottles", "set the drink window for this vintage".
https://cellarion.app/api/mcp (OAuth; your own cellar, read/write/consume scopes, sommelier and admin tools for those roles, with an action ledger and undo_last)https://cellarion.app/api/mcp/public (no signup; read-only shared wine registry)cellarion-mcp on npm and in the official MCP registry (app.cellarion/cellarion, app.cellarion/wine-registry)Self-hosted instances serve the same endpoints from their own origin — the /connect-ai page renders instance-specific snippets automatically.
A self-hosted Cellarion starts with an empty wine registry. The Registry Bridge connects it to the shared registry on cellarion.app, one wine at a time: registry matches appear in your add-bottle search, the wine you pick is copied into your own database with its tasting profile and drink windows, copies are refreshed weekly (your own edits to a copy are kept, and REGISTRY_BRIDGE_REFRESH=off stops the refresh), and the corrections and values your users file flow back to the shared queues, credited to your install. The registry itself never leaves cellarion.app — no bulk download, no snapshot.
.env and restart the backend:Quotas, the monthly import window and the protocol are described in docs/registry-bridge.md; the terms are at cellarion.app/terms.
| Entity | Description |
|---|---|
| WineDefinition | Vintage-neutral wine entry in the shared registry. Admin-managed; grows via user wine requests, imports, and sommelier correction proposals with one-click admin review. |
| Bottle | A user's bottle: references a WineDefinition and adds vintage, price, rating, notes, rack location, open/reserved state. |
| Cellar | Named container of Bottles, owned by a user. Can be shared with other users via role-based access. |
| Rack | Customizable grid (up to 20×20, default 8×4) within a Cellar for physical bottle placement, with a 3D room view. |
| WineVintageProfile | Sommelier-curated drink-window (maturity) data per wine + vintage — the source of drink alerts. |
| WineRequest | User-submitted wine suggestion. Admins review and fulfil by creating a WineDefinition. |
| Taxonomy | Admin-managed Countries, Regions, Appellations, and Grapes (with regional display names) to prevent free-text proliferation. |
| Notification | In-app + push notification for events like wine requests resolved, images approved, cellars shared, restocks. |
| SupportTicket | User support tickets with admin response tracking. |
| WineReport | User-submitted wine quality reports (wrong info, duplicates, inappropriate content). |
| Role | Description |
|---|---|
| user | Standard user — manages own cellars, bottles, and requests |
| sommelier | Curates maturity windows, pricing data, tasting profiles, and correction proposals for the shared registry |
| admin | Full access — wine library, taxonomy, user management, image review, registry quality queues, audit log |
| super admin | Platform-level access — system monitor, service health, rate limits, AI config, embedding management |
The backend exposes ~65 route modules; this is the core surface, not an exhaustive reference. Wine registry reads (/api/wines, public wine pages, OG images, sitemap) are public; everything personal requires a JWT (Authorization: Bearer <token>).
/api/auth| Method | Path | Description |
|---|---|---|
| POST | /register | Create account (sends verification email if Mailgun is configured) |
| POST | /login | Login, returns JWT (blocked until email is verified when Mailgun is configured) |
| GET | /google → /google/callback | Google SSO (when GOOGLE_CLIENT_ID/SECRET are set) |
| GET | /oidc → /oidc/callback | OIDC SSO against your own provider (when the OIDC_* vars are set) |
| GET | /sso/providers | Which SSO providers this deployment has configured |
| GET | /verify-email?token= | Verify email address, returns JWT on success |
| POST | /resend-verification | Resend verification email |
| POST | /forgot-password | Request password reset email |
| POST | /reset-password | Reset password with token |
/api/cellars (auth required)| Method | Path | Description |
|---|---|---|
| GET | / | List user's cellars |
| POST | / | Create cellar |
| GET | /:id | Get cellar + bottles |
| PUT | /:id | Update cellar |
| DELETE | /:id | Delete cellar |
| GET | /:id/statistics | Aggregated stats |
/api/bottles (auth required)| Method | Path | Description |
|---|---|---|
| POST | / | Add bottle to cellar |
| PUT | /:id | Update bottle |
| DELETE | /:id | Remove bottle |
| POST | /:id/consume · /:id/open · pour/close | Drink-tracking lifecycle |
| POST | /import/validate | Validate import data and match wines (registry-read-only) |
| POST | /import/confirm | Create bottles from validated import |
Writes on /api/bottles and /api/racks accept an optional Idempotency-Key header: a repeat with the same key gets the first answer back instead of being applied again (kept 8 days). Offline-queued writes also send preconditions (ifActive, ifUnchanged, expectOccupant/expectFrom/expectTo, ?expect=) and get 409 when the cellar changed meanwhile.
/api/offline (auth required)| Method | Path | Description |
|---|---|---|
| GET | /snapshot | The user's cellars (own + shared), unconsumed bottles and racks in one document, for offline mode (no-store, rate-limited) |
/api/winesPublic read. Regular-user searches are capped; admin/sommelier get full browse.
| Method | Path | Description |
|---|---|---|
| GET | / | Search/filter wines. Params: search, type, country, region, grapes, sort, limit, offset |
| GET | /:id | Get a single wine definition (grapes carry displayName regional labels) |
/api/chat (auth required)| Method | Path | Description |
|---|---|---|
| POST | / | Send a question to the AI cellar chat (RAG pipeline) |
/api/mcp and /api/mcp/publicSee Connect your AI. OAuth 2.0 with dynamic client registration on the personal server; the public server needs no auth.
/api/discussions, /api/reviews, /api/journal, /api/wishlist, /api/wine-lists, /api/follows, /api/recommendations, /api/restock-alerts — lists, reviews, discussions, journal, wishlist, follows, and restock alerts.
/api/climate (auth required)Register cellar sensors, ingest readings (Home Assistant-friendly token auth), per-cellar dashboards.
As before: /api/notifications, /api/stats/overview, /api/support, /api/wine-reports, /api/wine-requests.
/api/somm/* (somm or admin role)| Method | Path | Description |
|---|---|---|
| GET/PUT | /maturity | Curate drink-window phases per wine + vintage |
| GET/POST | /prices | Curate pricing data |
| PUT | /profile/:wineId | Correct a wine's tasting profile, type, and grapes |
/api/admin/* (admin role required)Wine definitions + merges, wine requests, correction proposals, taxonomy (incl. grape regional names), registry quality queues (duplicates, fragmentation, name checks, cross-field checks), images, users, tickets, audit log.
/api/superadmin/* (super admin only)Platform analytics, rate limits, AI model/prompt config, announcement banner, embedding jobs.
Copy .env.example to .env — it is fully commented and is the authoritative reference. Core values:
| Variable | Required | Default | Description |
|---|---|---|---|
JWT_SECRET | Yes | — | Long random string for signing JWTs |
MEILI_MASTER_KEY | Yes | — | Long random string for Meilisearch auth |
MONGO_URI | No | mongodb://mongo:27017/winecellar | MongoDB connection |
ACCESS_TOKEN_EXPIRES_IN | No | 15m | Access-token TTL (sessions refresh via a rotating 30-day cookie) |
PORT | No | 5000 | Backend port |
FRONTEND_URL | No | http://localhost | CORS origin — set to your domain in production |
MEILI_URL | No | http://meilisearch:7700 | Meilisearch URL |
REMBG_URL | No | http://rembg:5000 | Background removal service |
ANTHROPIC_API_KEY | No | — | Enables label scanning and AI cellar chat (get a key) |
VOYAGE_API_KEY | No | — | Required for AI cellar chat embeddings (get a key) |
QDRANT_URL | No | http://qdrant:6333 | Vector database URL (auto-set in Docker Compose) |
SUPER_ADMIN_EMAIL | No | — | Email of the super admin account |
MAILGUN_API_KEY / MAILGUN_DOMAIN | No | — | Enable email verification + transactional email |
Optional integrations (each fully documented in .env.example):
| Group | Variables | Enables |
|---|---|---|
| Self-hosted AI | AI_PROVIDER, OPENAI_BASE_URL, OPENAI_API_KEY, AI_MODEL, AI_VISION_MODEL, EMBEDDING_PROVIDER, EMBEDDING_MODEL, EMBEDDING_DIMENSION, … | Any OpenAI-compatible endpoint instead of Anthropic/Voyage — see Self-hosted AI |
| Google SSO | GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALLBACK_URL | Sign in with Google |
| OIDC SSO | required: OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_ISSUER, OIDC_AUTHORIZATION_URL, OIDC_TOKEN_URL, OIDC_USERINFO_URL · optional: OIDC_PROVIDER_NAME, OIDC_CALLBACK_URL, OIDC_SCOPES, OIDC_TRUST_EMAIL_VERIFIED | Sign in with your own identity provider (Pocket ID, Authentik, Keycloak, Zitadel, Authelia). Works alongside Google; endpoints come from the provider's /.well-known/openid-configuration. Register the client for client_secret_post — credentials go in the token request body, not HTTP Basic |
| Registry Bridge | REGISTRY_BRIDGE_URL, REGISTRY_BRIDGE_KEY, REGISTRY_BRIDGE_REFRESH | Search and copy wines from the shared registry on cellarion.app into a self-hosted install; REGISTRY_BRIDGE_REFRESH=off keeps copies exactly as copied — see Registry Bridge |
| Supporter payments | STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_{SUPPORTER,PATRON,BENEFACTOR}_PRICE_ID + _ANNUAL_ variants | Stripe Checkout for the optional tiers. Give each tier its own Stripe Product (one monthly + one yearly price), or the Customer Portal can't offer tier switching. Each price var takes a comma-separated list — current price first, then any retired prices that still have live subscribers |
| Push notifications | VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_EMAIL | Web-push for drink alerts and events |
| Climate monitoring | CLIMATE_RETENTION_DAYS, CLIMATE_MAX_DEVICES_PER_USER, CLIMATE_MAX_READINGS_PER_DAY, … | Sensor ingest limits + GDPR retention |
| Analytics | UMAMI_DB_PASSWORD, UMAMI_APP_SECRET, VITE_UMAMI_URL, VITE_UMAMI_WEBSITE_ID | Self-hosted cookie-free Umami (--profile analytics; VITE_* are build-time) |
| Ops | TRUST_PROXY_HOPS, COOKIE_SECURE, BACKEND_URL, MEILI_SEARCH_KEY, LOG_LEVEL, AUDIT_TTL_DAYS, VITE_SITE_URL, INDEXNOW_KEY, SUPER_ADMIN_IPS, QDRANT_API_KEY | Proxy trust, cookies, logging, SEO, audit retention |
The AI chat feature requires three services working together:
ANTHROPIC_API_KEY) — generates conversational responses grounded in your cellarVOYAGE_API_KEY) — creates wine embeddings for semantic searchQDRANT_URL) — vector database for fast similarity searchWhen all three are configured, users can ask natural-language questions about their collection (food pairings, occasion picks, cellar insights). The system only surfaces wines the user actually owns — no hallucinated recommendations.
A single daily usage quota — the same for every user, regardless of supporter tier — is configurable by SuperAdmins (default 50 questions/day).
Self-hosters who prefer not to use an Anthropic API key can point every LLM feature (cellar chat, label scan, import lookup, drink-window / price / profile suggestions) at any endpoint that speaks the OpenAI chat-completions API — Ollama, LM Studio, vLLM, LiteLLM, or OpenAI itself:
Wine embeddings (the semantic-search half of cellar chat) can independently be moved off Voyage AI the same way:
Notes:
host.docker.internal resolves out of the box only on Docker Desktop (Windows/macOS). On a Linux server, either add extra_hosts: ["host.docker.internal:host-gateway"] to the backend service in your compose file, or point OPENAI_BASE_URL at the host's LAN IP or a service on the compose network.0/-1 = unlimited) if you don't want your local endpoint metered.AI_MODEL); the configurable AI prompts still apply.EMBEDDING_PROVIDER, EMBEDDING_MODEL, or EMBEDDING_DIMENSION, run a full embedding job (SuperAdmin → AI) — it drops and rebuilds the collection at the new size. Every returned vector is validated against EMBEDDING_DIMENSION, so a wrong value fails loudly instead of corrupting search.When both MAILGUN_API_KEY and MAILGUN_DOMAIN are set, email verification is enabled:
/verify-email page.Existing users: After enabling verification on a running instance, existing accounts will have emailVerified: false and will be locked out. Run this once in the MongoDB shell to restore access:
Users can import bottles from other wine cellar apps (Vivino — including drinking history, CellarTracker, Ploc — including slot positions and purchase history, or any generic CSV). The import flow:
Import sessions are persisted so users can resume later if interrupted. Access the import from any cellar's overflow menu (⋯ → Import Bottles). Requires editor or owner access.
Bottles can also be imported as JSON. Each item supports:
To import directly into history (already consumed bottles), add:
Cellar owners can export their data via a cellar's overflow menu (⋯ → Export) or Settings. Available as JSON, or as a ZIP that also includes your uploaded bottle images. The export covers bottles with rack placement (rackName, rackPosition), rack geometry, the 3D room layout, and your own reviews. The JSON format is directly re-importable. (A full account export — every category of your data — is available under Settings → Privacy, per GDPR.)
When a wine is created, the system checks for near-duplicates using:
Score: name × 0.45 + producer × 0.45 + appellation × 0.10
Candidates above the threshold (default 0.75) appear as warnings with a "Use This" option. Behind that sit admin-side quality queues — duplicate groups, producer-fragmentation pairs, name checks, and cross-field domain checks (a producer that is actually an appellation, a region that is a country…) — re-tested against the live taxonomy on every scan.
Runs Vitest (vitest run — a single non-watch pass) with React Testing Library. Do not append CRA-style flags like --watchAll=false — the frontend is built with Vite, not Create React App, and unknown flags error.
Uses Jest — ~124 suites covering auth middleware, cellar access control, wine normalisation/dedup, registry checks, rating conversion, drink windows, MCP tools, and more.
Run both test suites before opening a pull request. PRs with failing tests will not be merged.
Contributions are welcome — bug fixes, features, documentation, and translations. CONTRIBUTING.md has the full guide: how to set up, the branch and test workflow, the conventions the codebase follows, the personal-data checklist every feature must pass, and how to sign your commits (Cellarion uses the Developer Certificate of Origin, so commit with git commit -s). The short version:
main (feat/, fix/, docs/…)cd frontend && npm test and cd backend && npm testdocker compose up --build, and use the thing you changedmain — the template asks for a description and a checklistEveryone taking part is expected to follow the Code of Conduct.
Cellarion's interface is community-translated with Weblate — a web editor, no Git and no coding required. English is maintained by the developers alongside the code; every other language comes from volunteers.
What's useful differs by language:
| Language | What it needs |
|---|---|
| French, German | Reviewers, not translators. Both were machine-drafted in bulk, so every field is already filled — the work is reading what's there and correcting it. The queue you want is state:translated NOT state:approved, not the untranslated one. |
| Swedish | A native reader. Effectively complete, barely reviewed. |
| Estonian, or a language not listed | Translators, from the top. Request the language in Weblate and start — it shows up in the app from roughly 10 % onwards, and ?lng=<code> previews it before that. |
Guidelines, the wine-terminology glossary, and how a language graduates out of beta: TRANSLATING.md.
(Please don't open pull requests that edit translation.json files directly — en excepted. They conflict with Weblate's two-way sync.)
Please do not open a public issue for a security problem. Use GitHub's private vulnerability reporting, or email info@cellarion.app with "SECURITY" in the subject. Response times, scope, and an honest account of the current security posture are in SECURITY.md.
GNU Affero General Public License v3.0 (AGPL-3.0)
You are free to use, modify, self-host, and even offer this software as a service. The one condition, and the reason Cellarion uses AGPL rather than MIT: if you run a modified version for other people, you must make your modified source available to them.
Contributions are accepted under the same license; see CONTRIBUTING.md.
This codebase was developed together with Claude Code by Anthropic.