WeGrocery # WeGrocery by [flylabs.ai](https://www.flylabs.ai/en) **Open-source, white-label platform for food co-ops and buying groups.** Members place orders inside weekly cycles and track a prepaid ledger balance; admins manage cycles, products, suppliers, top-ups, and analytics. One codebase, MIT licensed โ€” every group gets its own deployment with its own name, logo, colors, and language, configured by a single environment variable. | | | |---|---| | ๐Ÿงช **Try the public demo** | **[wegrocery-demo.vercel.app](https://wegrocery-demo.vercel.app)** โ€” one-click login, fake data | | ๐ŸŸข **In production** | [gas.portamoneta.org](https://gas.portamoneta.org) โ€” a Milan residents' association, members only | | ๐Ÿ“‹ **Changelog** | [English](./CHANGELOG.md) ยท [Italiano](./CHANGELOG.it.md) | ![WeGrocery โ€” member and admin walkthrough](./docs/demo.gif) --- ## What is this? WeGrocery runs the weekly purchasing cycle of a buying group: members order within a time-boxed window, the system tracks each member's prepaid balance like a ledger, and an admin closes the cycle, charges everyone, splits shipping, and emails the order to the supplier. It is **not a toy demo**: it has been in production since 2025 for [Porta Moneta](https://www.portamoneta.org), a Milan residents' association running a food-buying group (GAS, *Gruppo di Acquisto Solidale*) that uses it every week โ€” real money math, real concurrency control, real members. Porta Moneta is the first white-label deployment of this codebase. It is also a complete **reference implementation** of a modern Next.js app: App Router, React Server Components, Server Actions, Better Auth, Drizzle ORM on serverless Postgres, deployed on Vercel. **Who it's for:** - **Co-ops, GAS groups, buying clubs** that need an ordering + balance tool: run it yourself for free, or [get a turnkey instance](#want-this-for-your-group). - **Developers** looking for a production-grade Next.js 16 / Server Actions / Drizzle / Neon example to learn from โ€” contributions welcome. - **Anyone evaluating the work** โ€” recruiters, collaborators, potential clients. The app ships in **English and Italian** (one language per deployment, picked by the brand config); code, identifiers, comments, and docs are all in English. --- ## Try the demo [**wegrocery-demo.vercel.app**](https://wegrocery-demo.vercel.app) is a fully interactive demo running on fake data: 1. **One-click login, no signup.** Choose **"Enter as Member"** for the member view, or **"Enter as Admin"** for the admin panel. 2. **As a member:** place an order in the open cycle, then browse your balance, order history, ledger movements, and notifications. 3. **As an admin:** manage the cycle and products, record a top-up in the Treasury tab, review members, and open the analytics dashboard. Data is **fake and reset automatically every night**, so click around freely โ€” nothing you do is permanent. > The production site ([gas.portamoneta.org](https://gas.portamoneta.org)) is > **private**: login is restricted to the co-op's members and holds real data. > Use the demo to explore the app. --- ## White-label: run it for YOUR group **[docs/self-hosting.md](docs/self-hosting.md)** takes a group from nothing to a working installation in about 30 minutes, with a Deploy to Vercel button that creates the project and its database: no code to touch. Every deployment is branded by one env var, `NEXT_PUBLIC_BRAND_JSON`. No env var = the neutral WeGrocery brand you see in the demo. Example (every field: [docs/brand.example.json](docs/brand.example.json)): ```json { "appName": "Riverside Buying Club", "shortName": "Riverside", "locale": "en", "currency": "EUR", "logoUrl": "https://example.org/logo.png", "supportEmail": "hello@riverside.example", "theme": { "primary": "#2f9e44", "accent": "#1971c2" } } ``` Theme colours are hex (`#rgb` or `#rrggbb`). Give the fills only: the text colour on buttons and the darker shade used for coloured text are computed so that everything stays readable (WCAG AA). A palette that cannot reach that contrast, or a non-hex value, logs a `[brand]` warning at startup. Dates are shown and entered in `Europe/Rome` wall time. A group in another zone sets `NEXT_PUBLIC_TIME_ZONE` (an IANA name, e.g. `Europe/Lisbon`); it is inlined at build time, so redeploy after changing it. Onboarding a new group = one Vercel project + one Neon database + one brand JSON. Same `main` branch serves every deployment, so a bug fix or feature lands everywhere with a single merge โ€” no forks, no per-client branches. The full operating model is documented in [docs/operating-two-environments.md](./docs/operating-two-environments.md). --- ## Stack - **Next.js 16** App Router + React 19 + TypeScript strict mode - **Postgres** on [Neon](https://neon.tech) (HTTP driver, serverless) - **Drizzle ORM** with hand-written migrations - **Better Auth**: sign-in with an email link (optional Google), only for addresses in the `members` table (or with a valid membership card, where that check is set up) - **Tailwind CSS v4** with a custom theme (orange/teal/warm white) - **Vercel** for hosting and CI/CD (auto-deploy on push to `main`) No charting library, no UI kit, no state-management library. The whole admin analytics dashboard is rendered with pure CSS and inline SVG. --- ## Project structure ``` โ”œโ”€โ”€ app/ # Next.js App Router โ”‚ โ”œโ”€โ”€ page.tsx # Home: balance hero, open cycles, recent ledger โ”‚ โ”œโ”€โ”€ ordine/ # Order form (per-product +/- steppers) โ”‚ โ”œโ”€โ”€ storico/ # Order history + ledger movements โ”‚ โ”œโ”€โ”€ notifiche/ # In-app notifications โ”‚ โ”œโ”€โ”€ guida/ # FAQ โ”‚ โ””โ”€โ”€ admin/ # Admin panel with 7 tabs โ”œโ”€โ”€ components/ โ”‚ โ”œโ”€โ”€ app-shell.tsx # Layout wrapper (header, bottom nav on phones, top nav on desktop) โ”‚ โ”œโ”€โ”€ home/ # Home-only components โ”‚ โ”œโ”€โ”€ admin/ # One component per admin tab + shared modals โ”‚ โ””โ”€โ”€ ui/ # Button, Card, Toast, ConfirmDialog โ”œโ”€โ”€ lib/ โ”‚ โ”œโ”€โ”€ db/ โ”‚ โ”‚ โ”œโ”€โ”€ schema.ts # Drizzle schema โ”‚ โ”‚ โ”œโ”€โ”€ queries.ts # Read-only query helpers โ”‚ โ”‚ โ””โ”€โ”€ client.ts # Neon connection โ”‚ โ”œโ”€โ”€ actions/ # Server Actions (admin, order, notifications) โ”‚ โ””โ”€โ”€ auth/session.ts # requireUserSession(), requireAdmin() โ”œโ”€โ”€ drizzle/ # Hand-written SQL migrations โ”œโ”€โ”€ auth.ts # Better Auth instance and auth() โ”œโ”€โ”€ proxy.ts # Auth gate (redirect to /login) โ””โ”€โ”€ SETUP.md # Step-by-step local setup guide ``` --- ## Highlight features ### For members - **Balance hero card** โ€” the running ledger total, big and unmissable. Turns red when below zero with a one-click "top up" CTA. - **Live order draft** โ€” quantities update in real time alongside a "balance after order" projection so members never accidentally overspend. - **"Repeat last order"** โ€” one click prefills the cart with the previous cycle's order, matched by product identity (so it still works after the admin recreates products per cycle). - **"Next pickup" card** โ€” promotes the most-asked information ("when do I pick up?") to a prominent home card with a days-until counter. - **Notifications with per-member preferences** โ€” bell icon with unread badge, deep links to the relevant cycle, and a settings page where each member toggles app and email delivery per category (cycle opened, closing reminder, order charge, order updates, wallet top-up). A scheduled job emails a reminder shortly before a cycle closes to members who haven't ordered yet. ### For admins - **Atomic cycle close** โ€” uses a compare-and-swap `UPDATE ... RETURNING` so two admins clicking "close" at the same time can never produce duplicate ledger entries. If charge insertion fails mid-flow, the status flip is rolled back so the cycle can be retried cleanly. - **Shipping split modes** โ€” choose between flat per-member fee or proportional to each member's order value. The proportional mode rounds each share to two decimals and absorbs sum-of-cents drift on the largest order so the total stays exact and reruns are deterministic. - **Close with price adjustments** โ€” for weight-based items (e.g. "1 kg of salad" weighed at 1.2 kg) the admin opens a modal, edits the per-product unit price, and the system recomputes every order line and ledger entry before posting charges. - **Analytics dashboard** โ€” top 10 products, revenue trend across the last 12 closed cycles, supplier ranking, member engagement bands. Everything scoped to closed cycles only so the numbers are stable. - **Insight mini-cards** โ€” three at-a-glance metrics above the cycle list: cycles closing in the next 24h, members below zero balance, top product in the last 30 days. - **Supplier CSV export** โ€” one click downloads a UTF-8 CSV (with BOM, so Excel recognizes it) aggregated by product, ready to email to the supplier. - **Catalog reuse** โ€” supplier products live in a separate `supplier_products` table. When the price of a catalog item changes, the previous version is archived (not overwritten) so historical orders still resolve correctly. --- ## Architecture notes ### Server-first Every page is a React Server Component that fetches its own data via Drizzle queries. Mutations go through Next.js Server Actions with `requireUserSession()` / `requireAdmin()` guards โ€” the client never gets direct DB access and never receives sensitive credentials. ### Data model | Table | Purpose | |---|---| | `members` | Roster + role (`admin` / `attivi` / `utenti`) + email whitelist | | `order_cycles` | Time-boxed purchasing windows with shipping config | | `products` | Per-cycle catalog snapshot (so historical prices stay frozen) | | `supplier_products` | Reusable supplier catalog with price-change history | | `orders` | Line items: `(memberId, cycleId, productId) โ†’ quantity` | | `ledger_entries` | Append-only balance log: top-ups, order charges, shipping | | `notifications` | Per-member or per-role messages with read-at timestamp | | `notification_preferences` | Sparse per-member channel toggles (absent row = code default) | | `audit_log` | Append-only trace of admin actions | | `suppliers` | Supplier registry | ### Concurrency The HTTP driver (`@neondatabase/serverless`) doesn't support interactive transactions, so all multi-statement workflows use a CAS pattern: `UPDATE ... WHERE status='X' RETURNING ...` followed by per-row inserts, with a manual rollback (status flip back) if a follow-up step fails. ### No charting library The admin analytics tab draws four chart types โ€” horizontal bar, area+line trend, bar ranking, and a categorical split โ€” using only Tailwind utilities and inline SVG with a `viewBox`. Total bundle cost: zero. ### Brand layer and i18n All user-facing strings live in typed language packs (`lib/i18n/it.ts` / `en.ts` โ€” the English pack is type-checked against the Italian source, so a missing translation fails the build). Branding (name, logo, colors, locale, currency, support emails) comes from `NEXT_PUBLIC_BRAND_JSON`, validated fail-fast at build time in `lib/brand`. Dates and money are formatted with `Intl` using the deployment's locale and currency. The codebase itself (identifiers, comments, commits, docs) is in English. --- ## Local development See [SETUP.md](SETUP.md) for the full step-by-step setup (environment, database, a local admin, tests). Quick start once `.env.local` is in place: ```bash npm install npm run db:migrate # apply the migrations in drizzle/ to the database npm run dev # http://localhost:3000 ``` Other scripts: | Script | What it does | |---|---| | `npm run dev` | Dev server with hot reload (Next.js) | | `npm run build` | Production build + type check | | `npm run db:migrate` | Apply pending migrations (`drizzle/*.sql`) to the database in `.env.local` | | `npm run doctor` | What this environment has set up, and what is missing | | `npm test` / `npm run test:int` | Unit tests / integration tests on a test database | | `npm run db:studio` | Drizzle Studio (visual DB browser) | --- ## Deployment Pushing to `main` triggers a Vercel production deploy and `staging` gets a preview deployment; other branches are not deployed (`git.deploymentEnabled` in `vercel.json`). Migrations (`drizzle/*.sql`) are applied before the code that needs them, with `node scripts/db-migrate.mjs`, or at every production build with `MIGRATE_ON_BUILD=true`: see [docs/upgrading.md](docs/upgrading.md). What a deploy has set up shows in admin โ†’ Impostazioni โ†’ *Configuration status* and in `npm run doctor`. --- ## Contributing Contributions are welcome โ€” this is a real product used weekly by real groups, and improvements ship to every deployment. Start from **[CONTRIBUTING.md](CONTRIBUTING.md)**: how to report a bug, propose a feature, set up the project and open a pull request (against `staging`). Security issues go through [SECURITY.md](SECURITY.md), never a public issue; everyone follows the [Code of Conduct](CODE_OF_CONDUCT.md). If you run WeGrocery for your own group, a star and a note about your setup help a lot. --- ## License Code is [MIT licensed](./LICENSE) โ€” fork it, adapt it, run it for your own group. The "Porta Moneta" name, logo, and the `portamoneta.org` domain belong to APS Porta Moneta and are **not** covered by the license; client brand configurations are private to each deployment. --- ## Want this for your group? **Run it yourself** โ€” it's free and MIT licensed: clone, deploy, set your brand JSON. **Or get it done for you.** I set up turnkey, white-label instances โ€” your logo, colors, language, and domain, like the Porta Moneta deployment โ€” and build custom features on top. Get in touch: [LinkedIn](https://www.linkedin.com/in/federicodecillia) ยท [GitHub](https://github.com/federicodecillia) --- Built with [Claude Code](https://claude.com/claude-code) โ€” architected and reviewed by a human, executed by an agent.