尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Plan your party like an engineer. Configure drinks, cocktail recipes, and headcount — get a precise shopping list, cost range, break-even point, and a QR-based ticket workflow.
Run it free in your browser with no account, pay once for the hosted version, or deploy it to your own Cloudflare account for nothing. See Pricing.
App: https://bottlecount-epj.pages.dev — the product, free and paid tiers alike Docs: https://fre0grella.github.io/BottleCount — documentation only, no app
BottleCount helps you plan ticketed parties without spreadsheets or guesswork. You define who's coming, what they're drinking, how strong the event should be, and what things cost — the app calculates the shopping list and the economics from your menu structure.
The planning side needs no server and never will: presets ship with the app, your customizations live in the browser, and it works offline. What a server buys you is the part that is inherently shared — a link your guests can open, a party two people can run, and your data on more than one device.
| Browser | Hosted | Self-hosted | |
|---|---|---|---|
| Price | free, forever | one payment | free |
| Account | none | Google sign-in | Google, or a local sign-in |
| Your data | this browser only | your account | your Cloudflare account |
| Menu, shopping list, budget | ✅ | ✅ | ✅ |
| Custom ingredients and cocktails | ✅ | ✅ | ✅ |
| Guest list you type yourself | ✅ | ✅ | ✅ |
| Signed QR tickets, one scanner | ✅ | ✅ | ✅ |
| Shareable invite link | — | ✅ | ✅ |
| RSVP funnel and spread view | — | ✅ | ✅ |
| Co-organisers on one party | — | ✅ | ✅ |
| Sync across devices | — | ✅ | ✅ |
| Several phones on the door | — | ✅ | ✅ |
The free tier is not a trial: no expiry, no account, no card. If planning a party in one browser is all you need, that is the finished product.
Self-hosting is free because hosting is the thing being sold, not the software. Run the Worker yourself and there is nothing left to charge for — see Self-hosting.
The menu is built around three category patterns:
Alcohol intensity presets still map to pure alcohol targets per person: 🌿 Soft · 🍹 Aperitivo · 🎉 Party · 🔥 Hardcore.
TypeScript everywhere, and a frontend that still runs with the backend switched off.
| Layer | Technology |
|---|---|
| Language | TypeScript (strict) |
| Frontend | Astro + Vue 3, built with Vite |
| Client storage | Dexie.js on IndexedDB |
| Crypto | Web Crypto API (HMAC-SHA256) |
| QR generation | qrcode |
| QR scanning | nimiq/qr-scanner |
| API | Hono on Cloudflare Workers |
| Database | Cloudflare D1 |
| Auth | Google OAuth → HS256 JWT in an httpOnly cookie |
| Frontend host | Cloudflare Pages (docs on GitHub Pages) |
browser ──▶ Cloudflare Pages ──┬──▶ static Astro build
│
└──▶ Pages Function (functions/)
│ service binding, same origin
▼
Hono Worker ──▶ D1
/api/* and /auth/* are forwarded to the Worker over a service binding,
not a public URL. That is an internal dispatch, so the browser only ever talks
to one origin: the session cookie is first-party and no CORS preflight sits
between a user and signing in.
Both tiers are served from here. The free tier is not a different deployment —
it is the same app with nobody signed in, which is why GET /api/session
answers anonymous callers instead of rejecting them.
If the Worker is unreachable — it is down, or a self-hoster has not wired the
service binding up yet — the app degrades cleanly to the free tier instead of
failing. That is deliberate, and
src/features/account/session.ts is where
it is enforced.
The documentation site on GitHub Pages is a separate build that contains no application at all; see Deploying.
shared/tiers.ts is imported by both the Worker and the
frontend. A capability the UI hides but the API still serves is a paywall that
leaks; one the API refuses but the UI offers is a bug report. Both sides reading
the same table is the only version of this that stays honest.
BottleCount calculates how much pure alcohol your event needs, then distributes it through the configured drink structure.
The core formula for each spirit bottle count is:
[ \text{bottles} = \left\lceil \frac{N \times \text{alcohol target} \times \text{buffer} \times \text{macro%} \times \text{spirit%} \times \text{drink%}}{\text{ABV} \times \text{bottle ml}} \right\rceil ]
Mixer quantities are derived directly from cocktail recipes — if a drink uses 250 ml of a mixer per serving, the app multiplies that quantity by the total number of servings required.
These categories skip the cocktail layer and use the simpler formula:
[ \text{bottles} = \left\lceil \frac{N \times \text{alcohol target} \times \text{buffer} \times \text{macro%} \times \text{variety%}}{\text{ABV} \times \text{bottle ml}} \right\rceil ]
The break-even guest count is computed as:
[ N_{\text{be}} = \left\lceil \frac{\text{fixed costs}}{\text{ticket price} - \text{avg variable cost per person}} \right\rceil ]
git clone https://github.com/fre0grella/BottleCount
cd BottleCount
npm install # one lockfile; npm workspaces install backend/ too
Frontend only — the free tier, and all you need for anything on the planning side:
npm run dev
With the backend, in a second terminal:
cp backend/.dev.vars.example backend/.dev.vars # set JWT_SECRET to anything
npm run -w backend db:init:local # apply migrations to local D1
npm run backend:dev # wrangler dev --env local
Then open http://localhost:4321/app. The Astro dev server forwards /api/*,
/invite/* and the /auth/* routes to the Worker on :8787, and serves
/i/<slug> and /join/<token> — the same job the Pages Functions do on
Cloudflare — so the browser talks to one origin, exactly as in production.
Point it elsewhere with BACKEND_DEV_URL.
To try every paid feature:
BC-TEST-TEST-TEST. It is reusable, so a second account (for
co-organisers, say) can redeem it too.The local Worker environment behaves like the hosted product — you sign in as
free and the paywall is real. Put SELF_HOSTED="true" in
backend/.dev.vars to see the self-hosted behaviour instead, where every
signed-in user is pro.
Checks, all of which CI runs:
npm run lint
npm run typecheck # astro check + backend tsc
npm test # backend route and tier tests
npm run build
One-time setup:
npx wrangler d1 create db # paste the id into backend/wrangler.jsonc
cd backend
npx wrangler secret put JWT_SECRET --env production
npx wrangler secret put GOOGLE_CLIENT_SECRET --env production
npm run db:migrate:remote
npm run deploy:production # the Worker must exist before Pages
Then create a Pages project named bottlecount pointing at this repository, and
add the service binding in wrangler.toml (BACKEND → bottlecount-backend).
Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID as repository secrets and
.github/workflows/deploy-cloudflare.yml takes over from there. Without those
secrets the workflow skips rather than failing, so a fork stays green.
Your Google OAuth client's authorised redirect URI is <FRONTEND_URL>/auth/google
— the frontend origin, because the Pages Function proxies it back to the
Worker.
Push to main and .github/workflows/deploy.yml runs npm run build:docs,
which publishes the landing page, docs, pricing and legal pages under
/BottleCount/ — and leaves the application out. /app, /auth and /i
are deleted from that build, because each needs the Worker and there is none
behind GitHub Pages; a copy of the product that looks real and fails at sign-in
is worse than no copy.
Those five pages are built for both hosts, so each carries a
<link rel="canonical"> pointing at the Cloudflare copy — that domain is the
product, and it serves these pages as well as the app. Links to the app from
the docs point there too. Both come from one setting, PUBLIC_APP_ORIGIN
(src/lib/links.ts); a separate canonical origin and app
URL would be two settings obliged to name the same host.
The build settings live in the npm script, not the workflow, so this produces exactly what CI publishes:
npm run build:docs
Every paid feature is on, and it costs nothing beyond a Cloudflare account — the free plan is more than enough for a party.
git clone https://github.com/fre0grella/BottleCount
cd BottleCount && npm install
npx wrangler d1 create db # paste the id into the `selfhosted` env
cd backend
# in wrangler.jsonc, set env.selfhosted.vars.FRONTEND_URL to your Pages domain
npx wrangler secret put JWT_SECRET --env selfhosted
npm run db:migrate:remote
npx wrangler deploy --env selfhosted
Then deploy the frontend to Pages (npm run build && npx wrangler pages deploy dist)
and bind BACKEND to your Worker.
SELF_HOSTED=true promotes every signed-in user to the full feature set. It
does not promote anonymous visitors: co-organisers and the invite funnel need to
know who is who even when the server is yours. POST /auth/dev lets you sign in
without a Google client if you would rather not register one.
Until a checkout provider is wired up, fulfilment on the hosted tier is manual:
npm run -w backend licence:issue -- --env production --note "ko-fi #128"
It prints a code like BC-7K2M-QP4X-9DNR and inserts it into D1. The buyer
redeems it in the app, which flips their tier to pro.
For testing, BC-TEST-TEST-TEST unlocks pro on the local and preview
Workers without a row in D1 (TEST_LICENCE_CODE in backend/wrangler.jsonc).
It is refused on production whatever the config says, since the code is public.
BottleCount stores user data in the browser's IndexedDB through Dexie. Your custom ingredients, cocktails, settings, generated tickets and local app state stay on your device.
On the free tier that is the whole story — there is no account and nothing is uploaded, because there is nowhere to upload it to.
Signing in adds an account record (id, email, name, avatar URL, tier) in D1.
Nothing else is uploaded until you share a party — with a co-organiser, or with guests through an invite link. Doing either stores that party's planning document (name, date, venue, menu, settings, check-offs) so the people you shared it with can open it, plus a row per guest who RSVPs. Parties you have not shared stay in your browser and nowhere else, on every tier (ADR 0001, ADR 0002, ADR 0003).
Because browser storage is still local storage, export/import backup tools are an important part of the workflow for portability and recovery.
The backend has its own README covering routes, local setup, deployment and what the tests do and do not cover.
Licensed under PolyForm Noncommercial 1.0.0 — free for personal, educational and non-commercial use with attribution.
For commercial licensing, contact the author on GitHub.