The Hebrew–English family calendar. Every birthday, anniversary and yahrzeit — on both the Hebrew and the Gregorian date, in both Hebrew and English, shared with the whole family.
Jewish families run on two calendars at once. A birthday is on the 12th of Kislev and on some date in November that moves every year; a yahrzeit is fixed to the Hebrew date and quietly drifts through the civil one. Keeping that straight in a spreadsheet is how yahrzeits get missed. Luach keeps both dates for every occasion, shows whichever one you think in, and pushes the reminders out to the people who need them.
Hosted free at family-calendar.holzman-ai.com — or self-host it; the whole thing is in this repo.
The month grid: birthdays and anniversaries beside Rosh Chodesh and the candle-lighting times, with the next occasions down the side. Every person and date in these screenshots is from the fictional demo family that ships with the repo — see the demo family.
- Dual dates everywhere. Every event carries its Hebrew date and its Gregorian one. A birthday recurs on its Hebrew date and on its fixed civil date, so both show up — and Adar in a leap year, and 30 Cheshvan in a short year, land where they should.
- Yahrzeits, counted in Hebrew years (which is not the same as subtracting Gregorian years for a Tevet–Adar date that crosses January 1).
- Hebrew birthdays, including for people whose English birth date nobody remembers.
- Bilingual UI, Hebrew and English, with proper RTL. Every person can have a Hebrew name alongside their English one; each viewer picks their language and the whole site follows, calendar grid included.
- Yomim tovim and Shabbat built into the grid, so nothing sneaks up on you.
- Family tree — a zoomable, collapsible org chart of parents, spouses, remarriages and cousin marriages, tinted by family branch.
- Timeline and on this day views.
- Simchas — one-off gatherings (wedding, bar mitzvah, brit, upsherin, sheva brachot) with their own glyphs, separate from the recurring events.
- Subscribable calendar feed. Each family gets its own iCal URL, so every relative adds it once to Google or Apple Calendar and gets the birthdays and yahrzeits in their own calendar forever.
- WhatsApp digests and reminders — an optional weekly digest and a yahrzeit-eve reminder, sent via n8n and a WhatsApp gateway you run. See SETUP-WHATSAPP.md.
- Multi-family. One deployment hosts many unrelated families, isolated from each other by Postgres row-level security. If you belong to more than one — your side and your in-laws' — there is a combined view that merges both calendars with a colour per family.
- Google sign-in and invite links. No shared password. An owner mints an invite link; a relative opens it, signs in with Google, and is in.
- Printable QR cards with per-person magic links, for relatives who will never manage a login.
- Installable PWA with an offline shell.
- A Hebrew date converter, because someone always asks.
The family tree — three generations, spouses and parents, tinted by branch, zoomable and collapsible.
The same screen in Hebrew. Each viewer picks their own language; the whole site follows, right-to-left layout and Hebrew names included.
The landing page, for the relatives you still have to talk into it.
You need Node 20+ and a Postgres 13+ database.
Create the database and run every migration, as the schema owner:
createdb luach
DATABASE_URL="postgres://<owner>@localhost:5432/luach" npm run migrateThat applies scripts/seed.sql (the base schema) and every
scripts/migrate-v*.sql in order. All of them are idempotent, so re-running
after a git pull is how you upgrade.
Then create the restricted role the app actually connects as:
CREATE ROLE app_user LOGIN PASSWORD '<a strong password>';
GRANT USAGE ON SCHEMA family_calendar TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE
ON ALL TABLES IN SCHEMA family_calendar TO app_user;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA family_calendar TO app_user;
GRANT EXECUTE ON FUNCTION family_calendar.redeem_invite(TEXT, INTEGER) TO app_user;Families are kept apart by Postgres row-level security, and Postgres silently skips row-level security for superusers and for roles with
BYPASSRLS. PointDATABASE_URLatpostgres(or at the database owner) and nothing errors — every family just quietly gets read access to every other family's calendar. Run migrations as the owner; run the app asapp_user.
cp .env.example .env.localFill it in — .env.example documents each variable:
| Variable | Required | What it is |
|---|---|---|
DATABASE_URL |
yes | Postgres URL for the app_user role (see the warning above) |
SESSION_PASSWORD |
yes | 32+ chars sealing the session cookie — openssl rand -hex 32 |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
yes | Google OAuth 2.0 "Web application" client |
OAUTH_REDIRECT_URI |
yes | https://<your-domain>/api/auth/google/callback, registered identically in the Google console |
NEXTAUTH_URL |
in prod | Your public origin, used to build invite and feed URLs. The WhatsApp broadcast feeds 500 rather than guess if it is unset — there is no default |
FAMILY_BRANCHES |
optional | Your family's branch surnames, comma-separated — see Family branches. Unset = the demo family's |
N8N_TOKEN |
optional | Bearer token for the automation feeds — a deployment secret; it reads across families |
DIGEST_RECIPIENTS |
optional | Extra comma-separated E.164 numbers for digests |
A "branch" is a side of the family — usually a surname. Luach tints avatars, tree cards and timeline entries by branch, and each viewer can pick their preferred spelling of each branch surname. Set yours in the environment; no code changes:
FAMILY_BRANCHES="Levi,Cohen,Mizrahi,Adler,Other"Leave it unset and you get that fictional demo list.
Two rules:
- Keep a catch-all LAST. The final entry is the "no particular branch"
bucket. It is drawn in a neutral tint, it is where the CSV importer files
anyone whose surname it can't place, and it is left out of the
spelling picker (it has no surname to spell). Call it
Other,Misc,אחר— whatever you like; only its position matters. - Order is the colour. Branch colours are assigned by POSITION: the first name gets the first palette colour, the second the second, and so on. Reordering the list therefore reshuffles the colours everyone in your family already recognises — append, never insert or reorder. Renaming a branch in place keeps its colour, but does not rewrite the value stored on existing people; edit those in the app (or with SQL) if you want them to move.
You can list more or fewer than five. Beyond the built-in palette (four named
branches plus the neutral catch-all) colours repeat from the start; add entries
to BRANCH_STYLES in src/lib/branches.ts if you'd rather they didn't.
Branch values already in the database that are not in your current list don't break anything — those people render in the neutral tint, keep their stored value, and filter under the catch-all chip in the tree.
npm install
npm run dev # http://localhost:3000Sign in with Google and onboarding will create your family.
To see every feature working before you enter real people:
DATABASE_URL="postgres://app_user:...@localhost:5432/luach" npm run seed:exampleThis creates the fictional Levi family — three generations, a yahrzeit, Hebrew and English-dated birthdays, an anniversary, a bar mitzvah — and prints an invite link. Sign in, open the link, and you are in their calendar.
Most families already have one. Export it as CSV with the columns
Name, Hebrew Birthday, English Birthday, Anniversary[, Branch] and:
DATABASE_URL="..." npx tsx scripts/import-sheet.ts data.csv --family=<id>It parses the messy hand-written Hebrew dates people actually type
(ח' שבט, כ״ט ניסן, 15 Adar II) and reports anything it could not read so
you can add those few by hand. The parsing is
parse-hebrew-date, the
parser extracted from this project into its own package.
There is a Dockerfile producing a standalone Next.js build; it runs anywhere
that takes a container. Set the same environment variables as secrets, point
DATABASE_URL at your managed Postgres (as app_user), and run
npm run migrate against it once as the owner.
A few places carry the reference deployment's identity rather than yours:
- Branch surnames are the
FAMILY_BRANCHESenvironment variable — see Family branches. No code change needed. src/app/(marketing)/privacy/page.tsxandsrc/app/components/BuiltByHolzman.tsx— the privacy policy and footer name the hosted instance's operator and contact address. If you run your own public instance, put your own details there.
Next.js 16, React 19, Tailwind 4, Postgres (row-level security for tenancy),
@hebcal/core for the Hebrew calendar
maths, iron-session for the session cookie, ics for the calendar feed.
Tests are Vitest:
npm test
npx tsc --noEmit
npm run lintIssues and pull requests are welcome. This is a side project maintained by one person, so please note:
- No support SLA. Issues get answered when there is time. A clear reproduction gets answered faster.
- Open an issue before a large PR, so you don't build something that turns out to be a direction the project isn't going.
- Keep
npm test,npx tsc --noEmitandnpm run lintgreen. - Hebrew-calendar behaviour needs a test. The edge cases (Adar I/II, 30 Cheshvan and 30 Kislev in short years, Hebrew-year counts across January 1) are exactly where this software earns or loses its trust.
- Never commit real family data.
.gitignorealready excludes.env*and*.csv; keep fixtures fictional.
Security issues go to SECURITY.md, not the public tracker.
MIT © 2026 Shmuel Holzman.
Source: github.com/holzmanshmuel/luach · Hosted free at family-calendar.holzman-ai.com · Built by Holzman AI & Automations



