Caramel is the open‑source, privacy‑first alternative to Honey.
It automatically tests codes at checkout, never sells your data, and never overwrites creators’ affiliate links. Today Caramel already supports more than 5 000 stores. You can find even more information at https://grabcaramel.com.
-
100% open source – every release is on GitHub for public audit
-
Privacy first – the extension only asks for tab access while you shop, nothing more
-
Real‑time savings meter – see exactly how much a coupon saves you before you commit
-
Creator‑friendly – Caramel keeps existing affiliate links intact so influencers get the credit they deserve
-
Community‑powered – join our Discord, file issues, or open PRs to make Caramel even sweeter
- Chrome / Edge (Manifest V3)
- Firefox (AMO)
- Safari for macOS and iOS – converted from the Chromium build and re‑skinned automatically during CI
Prerequisites: Docker with Compose v2 (the one command that runs the app), plus Node.js 22+ and pnpm 9 (this repo's packageManager field — corepack enable picks it up) for installing deps, the escape hatches, and the test suites.
-
Install dependencies (repo root):
pnpm install
-
Create your env file:
cp apps/caramel-app/.env.example apps/caramel-app/.env
Then fill it in using the secrets table below — most values are already correct or optional.
-
Run it — one command builds the image and boots the whole stack:
pnpm dev
pnpm devisdocker compose up --build: it builds thewebimage, boots Postgres 18.4 + web, runsprisma migrate deployautomatically inside the container (creating and seeding the app-owned coupon catalog), and serves the app + API at http://localhost:58000. Local and CI run this samedocker-compose.ymlin prod-mode builds — and it is the deployment unit production migrates onto (cutover gated, human-run) — so what you run locally is what ships, which means hot reload is deliberately traded away (ratified 2026-07-09). When you want framework hot reload or to run one package on the host, bring up Postgres alone (docker compose up postgres -d) and use an escape hatch:pnpm dev:next # web app on the host (Next.js dev server, :58000, hot reload) pnpm dev:extension # the browser extension in a web-ext Chromium instance
Coupon routes return
200locally: the app owns its coupon catalog, created and seeded in the local Postgres byprisma migrate deploywhen the stack boots. In production the external pipeline keeps it fresh by pushing toPOST /api/ingest/catalog(seedocs/INGEST.md); the old externally-owned-DB "degraded mode" is retired (seedocs/LOCAL-DEV.md). -
Run the tests:
pnpm test # unit — real vitest, both packages (~300 tests) pnpm --filter caramel-app test:e2e # Playwright — needs Postgres up + migrations (docs/LOCAL-DEV.md) pnpm --filter caramel-app eval # cart-classifier AI eval — needs OPENROUTER_API_KEY, see apps/caramel-app/evals/README.md
apps/caramel-app/.env is gitignored and never committed — copy .env.example (step 2) and fill it in per this table.
DATABASE_URL — provided by local compose, but verify the value:
postgresql://caramel:caramel_password@localhost:58005/caramel?schema=public
This matches what .env.example ships — the compose Postgres creates exactly this caramel role (see docker-compose.yml).
COUPONS_DATABASE_URL — optional, bridge-sync only (leave unset locally):
The app serves its own coupon catalog from DATABASE_URL, so this is unset in local dev — the app never reads it at boot. It is consumed only by the migration-period bridge:sync job (pnpm --filter caramel-app bridge:sync), which reads the still-live external, Python-owned caramel_coupons Postgres (strictly read-only) and replays it into the app catalog through the same ingest engine as POST /api/ingest/catalog. It ships commented out in .env.example — uncomment it only when running that bridge against a reachable external DB (set-but-empty fails fast at boot by design). See docs/LOCAL-DEV.md and docs/INGEST.md.
Generate locally (any random string) — at least one of the first two is required:
| Variable | Notes |
|---|---|
JWT_SECRET |
|
BETTER_AUTH_SECRET |
|
EXTENSION_OAUTH_STATE_SECRET |
Only needed to test extension OAuth locally |
Local defaults — already correct in .env.example, no action needed:
| Variable | Shipped value |
|---|---|
BETTER_AUTH_URL |
http://localhost:58000 |
NEXT_PUBLIC_BASE_URL |
http://localhost:58000 |
BCRYPT_SALT_ROUNDS |
10 |
ALLOWED_ORIGINS |
blank (same-origin + extensions only) |
USESEND_BASE_URL |
https://usesend.devino.ca |
USESEND_FROM_EMAIL / USESEND_FROM_NAME |
no_reply@grabcaramel.com / Caramel |
OPENROUTER_MODEL |
openai/gpt-5-mini |
(NODE_ENV is deliberately absent from .env.example: it is framework-managed — Next.js sets it per command and env files cannot override it.)
Local-optional — leave blank unless you need the specific feature:
| Variable | Unlocks |
|---|---|
CHROME_EXTENSION_ORIGIN / FIREFOX_EXTENSION_ORIGIN / SAFARI_EXTENSION_ORIGIN |
Extension OAuth from a locally-loaded unpacked extension |
COUPONS_ADMIN_SECRET |
POST /api/coupons/expire (server-to-server) |
INGEST_API_KEY |
POST /api/ingest/catalog — the coupons pipeline supplier push (server-to-server) |
UPKUMA_HEALTH_SECRET |
GET /api/health/db — any value works, it just has to match |
API_ENCRYPTION_ENABLED / NEXT_PUBLIC_API_ENCRYPTION_ENABLED |
Response encryption — the two flags must agree |
Human-only — external provider dashboards, optional for a basic boot:
| Variable | Needed for |
|---|---|
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
Google sign-in |
APPLE_CLIENT_ID / APPLE_CLIENT_SECRET / APPLE_REDIRECT_URI |
Apple sign-in — see docs/APPLE_OAUTH_LOCAL_TESTING.md |
USESEND_API_KEY |
Outgoing email (signup verification, etc.) |
OPENROUTER_API_KEY |
The cart classifier (/api/classify-cart) and pnpm eval |
NEXT_PUBLIC_SENTRY_DSN |
Error/APM reporting (no-op locally without it) |
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID |
Analytics |
apps/caramel-app— Next.js web app + API (grabcaramel.com)apps/caramel-extension— browser extension (Chrome/Edge/Firefox/Safari)docker-compose.yml+Dockerfile— one-root-compose (web + Postgres);pnpm devruns itRUNBOOK.md— deploys, health checks, rollback, on-call
Full directory purposes: see Project layout below. Local infra detail: docs/LOCAL-DEV.md. Deploys/ops: RUNBOOK.md.
| Path | Purpose |
|---|---|
apps/caramel-app |
Web app + API for grabcaramel.com — Next.js, Prisma (auth DB), Better Auth |
apps/caramel-extension |
Browser extension source (Chrome/Edge/Firefox/Safari — no in-repo Xcode project; release CI packages Safari from dist/ via safari-web-extension-converter, see below) |
docker-compose.yml / Dockerfile |
One-root-compose: web + Postgres — the graph pnpm dev builds and runs — and the deployment unit production migrates onto |
The Safari Web Extension Converter (xcrun safari-web-extension-converter) automatically converts Chrome extension icons to Safari app icons, but it often adds white padding around them. .github/workflows/scripts/generate-safari-icons.sh and update-safari-icons.sh fix that: they generate and apply properly formatted Safari icons from a single source icon (apps/caramel-extension/icons/original.png).
Both scripts run only inside release-extension.yml's Safari publish job (macOS runner; needs ImageMagick + the Xcode project that job's own xcrun step generates) — there's no standalone local entry point. Read the workflow file if you need to reproduce a step by hand.
The project uses GitHub Actions for CI/CD. The workflow is defined in .github/workflows/.
For deploys, health checks, rollback, and known failure modes, see RUNBOOK.md.
See LICENSE file for details.