Find the cheapest groceries in Croatia. Disscount compares product prices across every major retail chain, shows real price history so you can tell whether a discount is genuine, and turns shopping lists into per-store basket totals.
Since May 15th 2025, Croatian retail chains are legally required to publish their product prices publicly. Disscount turns that raw open data (via the Cijene API) into clear comparisons, real price history and better shopping decisions. It runs in any browser and installs as an offline-capable PWA, so your lists and recently viewed prices keep working even with no signal in the store.
Under the hood it is a full production stack: a Next.js frontend that also acts as the identity provider (better-auth), a Spring Boot API that owns user data, and a shared PostgreSQL database, self-hosted on a Hetzner VPS via Dokploy behind Cloudflare. The codebase is source-available under BUSL-1.1 and documented (see docs/), and contributions are welcome.
Live:
- Product search across 29 Croatian retail chains (with barcode scanning)
- Price comparison per store and price history charts ("is the discount real?")
- Smart shopping lists with per-store basket totals
- Shared shopping lists via a private link, with view, shop or edit access and a revocable token
- Product watchlist
- Installable PWA that works offline (IndexedDB reads, background-sync writes)
- Google + email/password auth with account linking
Coming soon (marked with an USKORO badge in the app):
- Price-drop notifications
- Digital loyalty cards
- Store map with working hours
- Spending analysis and market statistics
- Frontend: Next.js, React, TypeScript, Tailwind CSS, shadcn-style UI on Radix + lucide-react, TanStack Query (+ Virtual), React Hook Form + Zod, Axios, Motion, Recharts, Sonner, cmdk, Vaul
- Backend: Spring Boot on Java, PostgreSQL, JPA / Hibernate, Spring Security + OAuth2 resource server, Bean Validation, Lombok, springdoc-openapi (Swagger UI), Actuator
- Auth: better-auth (in Next.js) issues ES256 JWTs, validated by Spring as an OAuth2 resource server via JWKS. Drizzle owns the auth tables
- PWA & offline: Serwist service worker, IndexedDB-backed React Query cache (idb-keyval), offline reads and queued writes, in-browser barcode scanning
- Email: Resend + React Email
- Infra: Docker Compose, Dokploy on a Hetzner VPS, Traefik, Cloudflare, Sentry, UptimeRobot, Umami
Deeper references live in docs/: authentication, PWA & offline, mobile navigation, state persistence, the landing page, brand assets, and deployment.
Deployed and available on: disscount.me
Live uptime and response times: status page
Landing page (full-page screenshot):
Created by: Jakov Jakovac
Big thanks to Cijene API for providing access to their API for data about products and store chains :)
If Disscount saves you money, you can support its hosting and further development on Ko-fi. Disscount stays free whether or not you choose to contribute.
This work is licensed under the Business Source License 1.1 (BUSL-1.1).
License parameters used in this repository:
- Licensor: Jakov Jakovac
- Additional Use Grant: None
- Change Date: Four (4) years from the date the Licensed Work is published
- Change License: GPL-3.0-or-later
Under BUSL-1.1 terms, each version converts on the Change Date or on the fourth anniversary of first publicly available distribution of that version, whichever comes first.
Commercial use is restricted unless covered by an Additional Use Grant or separate commercial terms from the Licensor.
The quickest way to run the full stack (frontend + backend + PostgreSQL) locally is with Docker Compose:
cp example.env .env # then fill in the values
docker compose up -d --build
# frontend: http://localhost:3000To run the services manually instead, follow the steps below.
- Node.js 24 LTS+ and pnpm
- Java 21 and Maven 3.9+
- PostgreSQL 14+ (17 in production)
- Docker + Docker Compose (only for the quick-run path above)
The backend and frontend share one PostgreSQL database: the backend owns the app tables (Hibernate ddl-auto), the frontend owns the better-auth tables (drizzle). Create the database, run the backend, then the frontend.
Create a database and user (example), and reuse these values in both env files below:
sudo -u postgres psql -c "CREATE USER disscount WITH PASSWORD 'secret';"
sudo -u postgres psql -c "CREATE DATABASE disscount OWNER disscount;"cd backend
cp .env.example .envSet in backend/.env:
SPRING_DATASOURCE_URL(e.g.jdbc:postgresql://localhost:5432/disscount)SPRING_DATASOURCE_USERNAMEandSPRING_DATASOURCE_PASSWORDBETTER_AUTH_JWKS_URI(e.g.http://localhost:3000/api/auth/jwks)
Build and run with the local Spring profile (it supplies local better-auth defaults, the JWKS URL and issuer at http://localhost:3000, so the app starts without extra config):
mvn clean install
mvn spring-boot:run -Dspring-boot.run.profiles=localThe backend runs on port 8080. Swagger UI: http://localhost:8080/api-docs
cd frontend
cp .env.local.example .env.localSet in frontend/.env.local (see the file for the full list):
DATABASE_URL(same Postgres DB, e.g.postgresql://disscount:secret@localhost:5432/disscount)BETTER_AUTH_SECRET(strong random string) andBETTER_AUTH_URL=http://localhost:3000NEXT_PUBLIC_API_URL=http://localhost:8080andNEXT_PUBLIC_APP_URL=http://localhost:3000- Google OAuth (
NEXT_PUBLIC_GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET) and Facebook (FACEBOOK_CLIENT_ID,FACEBOOK_CLIENT_SECRET) RESEND_API_KEY+EMAIL_FROM, andCIJENE_API_URL+CIJENE_API_TOKEN
Install dependencies, create the better-auth tables, then run:
pnpm install
pnpm drizzle-kit migrate # creates the better-auth tables in the shared DB
pnpm devThe frontend runs on http://localhost:3000
The repo ships pre-configured MCP servers so an AI editor picks them up on clone, one file per tool (same servers): .mcp.json (Claude Code), .cursor/mcp.json (Cursor), .vscode/mcp.json (VS Code).
| Server | Purpose |
|---|---|
playwright |
Browser automation / E2E |
context7 |
Up-to-date library docs |
supabase |
Supabase project access |
chrome-devtools |
Chrome DevTools debugging |
sentry |
Error tracking (Disscount) |
No secrets are committed: supabase and sentry authenticate via browser OAuth on first use, and context7 runs keyless (set CONTEXT7_API_KEY in your environment to raise rate limits). Never put keys in these files - use env vars or your editor's secret store.
playwright needs a browser at runtime: run npx playwright install chromium once per machine, or point it at an existing browser via PLAYWRIGHT_MCP_EXECUTABLE_PATH (e.g. /opt/helium/chrome). Both are machine-local, so keep the browser path out of the committed config.
Disscount is self-hosted on a Hetzner VPS using Dokploy (Docker Compose), with Traefik for routing and automatic Let's Encrypt TLS, and Cloudflare in front for DNS, CDN, and proxying. Production deploys automatically from the main branch, and a staging environment deploys from the dev branch, on every push. Runtime errors go to Sentry, and UptimeRobot polls /health with a public status page.
See docs/DEPLOYMENT.md for the full infrastructure reference: architecture, environment variables, DNS and SSL, security, backups and restore, monitoring, and how to add more apps to the server.
Contributions are welcome, whether it's a bug report, feature idea, documentation improvement or code change. See the Contributing guide for how to report issues, set up your environment, and open a pull request.
Please also review our Code of Conduct. To report a security vulnerability, follow the Security Policy rather than opening a public issue.
Thank you for helping improve Disscount - every contribution helps!















