Skip to content

t4n15hq/ember

Repository files navigation

Ember

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.


What Ember is

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.

Try it

  1. Go to ember.recipes
  2. Sign in with Google
  3. Create your first cookbook, invite the people you want to share it with
  4. Start adding the recipes that actually deserve to be remembered

Features

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

For developers

This repo is the source for the Ember service. It's open source under MIT.

Stack

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

Architecture highlights

  • 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-image renders per-cookbook preview cards via next/og — when you text an invite link, it previews with the cookbook name, member count, and mesh-gradient cover.
  • Env validation. src/lib/env.ts validates process.env at 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/health returns DB status for uptime monitors.
  • Zero external services beyond Postgres, R2, and Google OAuth. No analytics, no feature flags, no error-tracking SaaS.

Project structure

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

Running locally

# 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 dev

Open http://localhost:3000, sign in with Google, create your first cookbook.

Scripts

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

Security model

  • 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.ts and the Caddyfile.
  • .env* gitignored; src/lib/env.ts refuses to boot in production if Google OAuth is unconfigured.

Deploying your own instance

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)

Prerequisites (Debian/Ubuntu)

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 caddy

App

sudo 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 $HOME

Caddy

Add to /etc/caddy/Caddyfile: import /opt/ember/Caddyfile, then sudo systemctl reload caddy. TLS auto-provisions via Let's Encrypt.

Backups

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>&1

Updating

cd /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 ember

Roadmap

Shipped, 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 (current ILIKE is 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

License

MIT © 2026 Tanishq Padwal


Built because the biryani recipe nobody can find kept costing us Sunday lunch.

About

A private, invite-only recipe archive for family and friends. Live at ember.recipes. Next.js 16 · Drizzle · Postgres · Auth.js · Tailwind · R2.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages