A private, invite-only recipe archive for family and friends. No feed, no algorithm, no strangers — just the dishes worth keeping, in one beautiful place you'll still have in ten years.
Live at ember.recipes.
Ember solves a specific, boring problem: recipes worth keeping — your nani's biryani, your mum's pav bhaji, the neighbor's sourdough, the chicken stew that saves every weeknight — live in WhatsApp messages, group chats, and text threads. They get buried. Ten years later, they're gone.
Ember is the single, permanent place for them, visible only to the people you invite. It's designed for 5 to 50 people — a family, a friend group, a supper club. Not 5 million.
- Invite-only. No public profiles. No discoverability. No algorithm.
- Mobile-first. Made for people with flour on their hands.
- Your recipes stay yours. Attribution, notes, and photos stay attached to the author.
- Free, no ads.
- Go to ember.recipes
- Sign in with Google
- Create your first cookbook, invite the people you want to share it with
- Start adding the recipes that actually deserve to be remembered
| Private cookbooks | Grouped by household, friend circle, supper club — each with its own invite link |
| Recipe authoring | Drag-to-reorder ingredients + steps, drafts auto-save every 30s |
| Cook mode | Full-screen step view, embedded timers, swipe-between-steps, screen wake-lock so your phone never sleeps mid-cook |
| Attribution | Every recipe keeps its author and optional story — "adapted from Nani's stovetop version" is a first-class field |
| Notes | Comments on recipes from people in the cookbook — swaps, timings, how it went |
| Favourites | Personal bookmarks for recipes you rotate back to |
| Servings scaler | Live-adjust quantities up or down |
| Image uploads | Cover photos compressed to WebP, served from Cloudflare R2 |
| PWA | Installable on iOS/Android home screens, offline-capable shell |
| Rich invite previews | Dynamic OG images — share an invite link in iMessage or WhatsApp and it previews beautifully |
| Dark mode | Automatic, warm and candlelit |
This repo is the source for the Ember service. It's open source under MIT.
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 16 (App Router) | Server Components let most pages render directly from the DB — no API round-trips for the happy path |
| Language | TypeScript | End-to-end type safety, shared Zod schemas between client + server |
| ORM | Drizzle ORM | SQL-like, zero runtime overhead, great TS inference |
| DB | PostgreSQL 17 | Relational model fits the domain cleanly |
| Auth | Auth.js v5 + Drizzle adapter | Google OAuth with DB-backed sessions |
| UI | Tailwind CSS 4 + custom design system | Warm paper aesthetic, handcrafted mesh gradients |
| Icons | lucide-react | Clean, consistent set |
| Images | Cloudflare R2 + Sharp | S3-compatible cheap object storage, server-side compression to WebP |
- Server-first. Most pages are React Server Components reading from Drizzle directly. The client bundle is small; the API exists only for client-driven mutations (favourite toggle, comment form, image upload).
- Typed data layer. Every query lives in
src/lib/data/*as a typed function. Every mutation runs in a transaction. Authorization checks (cookbook membership, recipe authorship) happen server-side before any work. - Strict authorization model. Three roles: owner (can delete cookbook + remove members), contributor (can add recipes + comment), viewer (read-only). Checks are explicit in every API route — no permission-through-obscurity.
- In-memory rate limiting. Token-bucket per user per endpoint. Comments 10/min, invites 6/min, recipes 6/min, uploads 20/min, favourites 40/min. Returns 429 +
Retry-After. - Dynamic OG images.
/join/[code]/opengraph-imagerenders per-cookbook preview cards vianext/og— when you text an invite link, it previews with the cookbook name, member count, and mesh-gradient cover. - Env validation.
src/lib/env.tsvalidatesprocess.envat first access and refuses to boot in production if something is unsafe (e.g. Google OAuth missing). - Built-in observability. Structured JSON logs in production, tinted in dev. Request IDs on every mutation.
/api/healthreturns DB status for uptime monitors. - Zero external services beyond Postgres, R2, and Google OAuth. No analytics, no feature flags, no error-tracking SaaS.
src/
├── app/ Next.js App Router — route groups for (auth), (main), (legal)
│ ├── api/ Server actions + route handlers
│ └── join/[inviteCode]/ Public invite landing (with dynamic OG image)
├── components/ UI components, grouped by domain (recipes/, cookbooks/, shared/)
├── lib/
│ ├── auth.ts Auth.js v5 config
│ ├── db/ Drizzle client + schema
│ ├── data/ Typed data access layer
│ ├── rate-limit.ts In-memory token bucket
│ ├── storage.ts R2 upload pipeline
│ └── env.ts Runtime env validation
└── types/ Shared TypeScript types
# 1. Postgres
docker compose up -d
# starts an `ember-postgres` container on host port 5433
# 2. Env
cp .env.example .env.local
# Fill in DATABASE_URL, AUTH_SECRET, AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET, R2_*.
# 3. Install + apply schema
npm install
npm run db:generate
psql "$DATABASE_URL" -f drizzle/0000_*.sql
# 4. Go
npm run devOpen http://localhost:3000, sign in with Google, create your first cookbook.
| Command | What it does |
|---|---|
npm run dev |
Dev server |
npm run build |
Production build |
npm run start |
Run the built app |
npm run typecheck |
tsc --noEmit |
npm run db:generate |
Generate SQL migration files from schema changes |
npm run db:push |
Interactive schema sync |
npm run db:studio |
Drizzle Studio GUI |
- Google OAuth only. No passwords to leak.
- DB-backed sessions (not JWTs) so sign-out is instant + revocation works.
- Every mutation validates membership or authorship before running.
- Rate-limited at the route level.
- No third-party trackers or analytics. Strict security headers via
next.config.tsand the Caddyfile. .env*gitignored;src/lib/env.tsrefuses to boot in production if Google OAuth is unconfigured.
Though Ember is designed to run as a single hosted service at ember.recipes, the code is MIT licensed and deploys cleanly to any VPS. The Dockerfile, docker-compose.yml, Caddyfile, and scripts/backup-db.sh in this repo are the full production setup.
Ops runbook (click to expand)
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.nvm/nvm.sh && nvm install 22
npm install -g pm2
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install -y caddysudo mkdir -p /opt/ember && sudo chown $USER /opt/ember
cd /opt/ember
git clone https://github.com/t4n15hq/ember .
npm ci
export POSTGRES_PASSWORD="$(openssl rand -hex 24)"
echo "POSTGRES_PASSWORD=$POSTGRES_PASSWORD" | sudo tee /etc/default/ember-postgres
docker compose up -d
npm run db:generate
DATABASE_URL="postgresql://ember:$POSTGRES_PASSWORD@localhost:5433/ember" \
psql "$DATABASE_URL" -f drizzle/0000_*.sql
cat > /opt/ember/.env.production <<EOF
NODE_ENV=production
NEXT_PUBLIC_APP_URL=https://your-domain.example
DATABASE_URL=postgresql://ember:$POSTGRES_PASSWORD@localhost:5433/ember
AUTH_SECRET=$(openssl rand -base64 32)
AUTH_TRUST_HOST=true
AUTH_GOOGLE_ID=<your client ID>
AUTH_GOOGLE_SECRET=<your client secret>
R2_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
R2_ACCESS_KEY_ID=<your R2 access key>
R2_SECRET_ACCESS_KEY=<your R2 secret>
R2_BUCKET_NAME=ember-images
R2_PUBLIC_URL=https://pub-<hash>.r2.dev
EOF
chmod 600 /opt/ember/.env.production
npm run build
pm2 start npm --name ember --env production -- start
pm2 save
pm2 startup systemd -u $USER --hp $HOMEAdd to /etc/caddy/Caddyfile: import /opt/ember/Caddyfile, then sudo systemctl reload caddy. TLS auto-provisions via Let's Encrypt.
curl https://rclone.org/install.sh | sudo bash
rclone config create r2 s3 \
provider=Cloudflare \
access_key_id=<R2 access key> \
secret_access_key=<R2 secret> \
region=auto \
endpoint=https://<account-id>.r2.cloudflarestorage.com
# Cron at 03:12 daily
crontab -e
# add:
# 12 3 * * * R2_RCLONE_TARGET=r2:ember-backups /opt/ember/scripts/backup-db.sh >> /var/log/ember-backup.log 2>&1cd /opt/ember
git pull
npm ci
npm run build
npm run db:generate && psql "$DATABASE_URL" -f drizzle/000X_*.sql # if schema changed
pm2 restart emberShipped, but not yet surfaced everywhere in the UI:
- Recipe cover images in the list grid
- Step images in cook mode
- "I made this!" reaction with photo
Under consideration:
- Full-text search via Postgres
tsvector(currentILIKEis fine at scale) - URL import — paste a food-blog link, auto-parse the recipe
- Shopping list generated from multiple recipes
- Weekly email digest ("3 new recipes in Family Table this week")
- Export cookbook → PDF
MIT © 2026 Tanishq Padwal
Built because the biryani recipe nobody can find kept costing us Sunday lunch.