A shared groceries list you control from Telegram and a mobile-friendly web UI, backed by Neon Postgres.
- Telegram bot via webhook (no polling)
- Next.js App Router + TypeScript + Tailwind CSS
- Drizzle ORM over Neon Postgres
- A single shared service layer drives both the bot and the web UI — no duplicated business logic
- One Telegram chat (private or group) = one shared workspace
Each Telegram chat_id maps to a workspace. Everyone in a group shares the
same lists. The bot replies to /start with a private web link
(/w/<token>) so you can manage lists in the browser too. The web link uses an
unguessable token instead of a numeric id — there is no login in this MVP.
| Concern | Choice |
|---|---|
| Framework | Next.js (App Router) |
| Language | TypeScript |
| Package manager | pnpm |
| Styling | Tailwind CSS v4 |
| Database | Neon Postgres |
| ORM | Drizzle ORM + drizzle-kit |
| Telegram | Bot API webhook (sendMessage) |
| Tests | Vitest |
pnpm install- Sign up at neon.tech and create a project.
- Copy the connection string (use the pooled/serverless URL).
- Put it in
.envasDATABASE_URL.
- In Telegram, talk to @BotFather and run
/newbot. - Copy the bot token into
.envasTELEGRAM_BOT_TOKEN. - Choose any long random string for
TELEGRAM_WEBHOOK_SECRET.
Copy the example file and fill it in:
cp .env.example .envDATABASE_URL=postgresql://... # Neon connection string
TELEGRAM_BOT_TOKEN=123456:ABC-DEF... # from @BotFather
TELEGRAM_WEBHOOK_SECRET=long-random-str # you choose this
NEXT_PUBLIC_APP_URL=https://your-app... # public base URL (no trailing slash)Generate is already committed in lib/drizzle/. To apply the schema to your
database:
pnpm db:migrate # apply committed migrations
# or, for quick local iteration:
pnpm db:push # push schema directly (no migration files)To regenerate migrations after changing lib/db/schema.ts:
pnpm db:generatepnpm devApp runs at http://localhost:3000.
Telegram must reach your app over HTTPS. Deploy the app (e.g. to Vercel) or expose it locally with a tunnel, then register the webhook.
With TELEGRAM_BOT_TOKEN, TELEGRAM_WEBHOOK_SECRET, and NEXT_PUBLIC_APP_URL
set in your environment:
pnpm set-webhookhttps://api.telegram.org/bot<token>/setWebhook?url=<app-url>/api/telegram/webhook&secret_token=<secret>
The webhook route validates the X-Telegram-Bot-Api-Secret-Token header against
TELEGRAM_WEBHOOK_SECRET on every request.
Telegram cannot call localhost. Use a tunnel such as ngrok:
ngrok http 3000
# set NEXT_PUBLIC_APP_URL to the https URL ngrok prints, then:
pnpm set-webhook/start create workspace + default list, get web link
/help list all commands
/add milk, eggs, bread add items (comma-separated => multiple items)
/list show the current list, grouped by category
/done milk mark bought (also accepts the item id, e.g. /done 12)
/undone milk mark pending
/urgent eggs mark urgent 🔥
/noncritical napkins mark non-critical ▫️
/normal eggs reset priority
/remove bread delete an item
/lists show all lists
/newlist Weekly shop create + switch to a new list
/use Weekly shop switch current list (id or name)
/close close (archive) the current list
/baselists show templates
/savebase Staples save the current list as a reusable template
/recreate Staples start a new list from a template
Items resolve by id first, then by name (case-insensitive). If a name is ambiguous, the bot shows the matching ids instead of guessing.
/— landing page (explains Telegram control)/w/<token>— workspace: see lists, create, switch current, close/w/<token>/lists/<listId>— items grouped by category; check/uncheck, edit, delete, change priority and category. Closed lists are read-only./w/<token>/base-lists— manage templates; recreate a list from a template
Within a category items are ordered urgent → normal → non-critical → bought. Bought items are de-emphasized; urgent highlighted; non-critical softened.
app/
page.tsx landing
not-found.tsx
actions.ts server actions -> service layer
api/telegram/webhook/route.ts webhook: validate secret -> dispatch -> 200
w/[token]/ web UI (workspace, lists, base-lists)
lib/
db/{index.ts, schema.ts} Drizzle + neon-http
drizzle/ generated migrations
grocery/{service.ts, repository.ts, types.ts} shared business logic
telegram/{client.ts, commands.ts, handle-update.ts, render.ts, types.ts}
env.ts validated env
scripts/set-webhook.ts webhook registration helper
tests/ vitest unit tests
pnpm dev # run the dev server
pnpm build # production build
pnpm start # run the production build
pnpm lint # eslint
pnpm typecheck # tsc --noEmit
pnpm test # vitest run
pnpm db:generate # generate a migration from the schema
pnpm db:migrate # apply migrations
pnpm db:push # push schema directly (dev)
pnpm set-webhook # register the Telegram webhookThe data model and service layer are designed so AI features can be added later without restructuring: automatic category detection, item-name normalization, Hebrew/English grocery mapping, and image-based recognition from photos. For now category assignment is manual and editable.