eInvit / Technical Documentation
Technical Documentation

Complete architectural overview, deployment guide, API reference, and operations manual for eInvit v123 β€” Kerala & India's digital event invitation platform. Built entirely on Cloudflare's free stack.

βœ…
Current Version: v123 Live at einvit.in Β· Admin at admin.einvit.in Β· API at api.einvit.in Β· Developer: Tony Augustine Labs (TAL)
πŸ“
What's new since v61 Corporate Events added as an 8th community (Product Launch, Conference/Summit, Annual Day, Award Ceremony, Team Offsite β€” 70+ event types platform-wide) Β· WhatsApp delivery switched from the paid Meta Cloud API to free wa.me click-to-chat links (no WABA approval needed) Β· Bulk guest email via MSG91 alongside Resend Β· Digital Gift Registry + itemised Gift Wish List Β· Hero video, "How We Met" love-story timeline, vendor credits, live day-of updates, save-the-date toggle, and QR code Β· Guest arrival check-in tool Β· On-device AI Story Writer and DOCX Card Maker Studio (both zero-cost, no external AI API) Β· Curated platform-wide real-wedding gallery Β· 15 enriched demo invitations across all communities and plans Β· two-tier (in-memory + KV) caching and D1 batch-query performance pass (see Performance & Caching). See Release Highlights for the full list.

πŸ—οΈ Architecture Overview

eInvit is a three-package Cloudflare-native monorepo. All compute runs at the edge β€” no traditional servers, no Docker, no VMs. The entire platform runs on Cloudflare's free tier.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ CLOUDFLARE NETWORK (EDGE) β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Cloudflare Pages β”‚ β”‚ Cloudflare Workers β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ einvit.in │───▢│ api.einvit.in β”‚ β”‚ β”‚ β”‚ (Astro SSR) β”‚ β”‚ (Hono REST API) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ admin.einvit.in β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ (React SPA / Vite) β”‚ β”‚ β”‚ D1 β”‚ β”‚ Cloudinaryβ”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ (SQLite) β”‚ β”‚ (Media) β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚ KV (Sessions + Cache) β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ + in-memory L1 cache β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β–Ό β–Ό β–Ό wa.me click-to-chat Razorpay Payments MSG91 (bulk email) (free β€” no API key, (Indian gateway) Resend (password no Meta approval) reset / expiry)

βš™οΈ Tech Stack

ComponentTechnologyPurposeFree Tier Limits
Public siteAstro 4 (SSR) + React IslandsEvent invitation pages + landingUnlimited static / Pages
Admin dashboardReact 18 + Vite + Zustand + TanStack QueryEvent management SPA (installable PWA)Unlimited / Pages
APICloudflare Worker + Hono 4 + ZodREST backend100,000 req/day
DatabaseCloudflare D1 (SQLite)All app data5M row reads/day, 100K writes/day
Sessions & CacheCloudflare KV + in-memory (per-isolate)JWT sessions, two-tier plan-config & page cache100K reads/day, 1K writes/day
MediaCloudinary (free tier)Photos, videos, music β€” CDN delivery25 credits/month
PaymentsRazorpayIndian payments (UPI, Cards, NetBanking)2% per transaction
WhatsAppwa.me click-to-chat linksPersonalised guest invitations β€” free, no WABA approval (since v68)Unlimited, no API key
Bulk EmailMSG91 Email APIGuest-facing bulk invite emails (since v68)Larger free monthly quota
Transactional EmailResendPassword reset, expiry warnings3,000 emails/month
OG Images@resvg/resvg-wasmSocial preview images β€” CDN-cached 7 days via Cache APIEdge-rendered, no limit
AuthHMAC-SHA256 JWT + PBKDF2Web Crypto API β€” no external depsβ€”
Monorepopnpm workspacesUnified dependency managementβ€”
TypeScript5.7+End-to-end type safety (fully typed since v54)β€”
Wrangler4.40+Cloudflare CLI (deploy, dev, D1, KV)β€”

πŸ“ Project Structure

einvite/
β”œβ”€β”€ apps/
β”‚   β”œβ”€β”€ public/           # Astro SSR β€” public invitation pages + landing
β”‚   β”‚   └── src/
β”‚   β”‚       β”œβ”€β”€ pages/
β”‚   β”‚       β”‚   β”œβ”€β”€ index.astro           # Landing page (8 communities incl. Corporate)
β”‚   β”‚       β”‚   β”œβ”€β”€ w/[slug]/index.astro  # Event invitation page
β”‚   β”‚       β”‚   β”œβ”€β”€ card-studio.astro     # Card Maker Studio entry point
β”‚   β”‚       β”‚   β”œβ”€β”€ privacy.astro
β”‚   β”‚       β”‚   β”œβ”€β”€ terms.astro
β”‚   β”‚       β”‚   └── refund.astro
β”‚   β”‚       β”œβ”€β”€ public/
β”‚   β”‚       β”‚   β”œβ”€β”€ user-manual.html      # User guide β€” v123
β”‚   β”‚       β”‚   β”œβ”€β”€ technical-docs.html   # You are here β€” v123
β”‚   β”‚       β”‚   └── chatbot/              # Local-KB assistant widget (v89+), no external AI API
β”‚   β”‚       β”œβ”€β”€ layouts/
β”‚   β”‚       β”‚   └── WeddingLayout.astro   # Theme CSS injection per template
β”‚   β”‚       β”œβ”€β”€ env.d.ts                  # PUBLIC_API_URL typed (v54)
β”‚   β”‚       └── components/
β”‚   β”‚           β”œβ”€β”€ blocks/               # Server-rendered Astro components
β”‚   β”‚           β”‚   β”œβ”€β”€ HeroSection.astro
β”‚   β”‚           β”‚   β”œβ”€β”€ EventTimeline.astro
β”‚   β”‚           β”‚   β”œβ”€β”€ CoupleStory.astro
β”‚   β”‚           β”‚   └── VenueMap.astro
β”‚   β”‚           └── islands/              # Hydrated React islands
β”‚   β”‚               β”œβ”€β”€ CountdownTimer.tsx
β”‚   β”‚               β”œβ”€β”€ RSVPForm.tsx
β”‚   β”‚               β”œβ”€β”€ PhotoGallery.tsx
β”‚   β”‚               β”œβ”€β”€ Guestbook.tsx
β”‚   β”‚               β”œβ”€β”€ ShareButtons.tsx
β”‚   β”‚               └── MusicPlayer.tsx
β”‚   β”‚
β”‚   └── admin/            # React SPA β€” admin dashboard (installable PWA, sw.js)
β”‚       └── src/
β”‚           β”œβ”€β”€ pages/
β”‚           β”‚   β”œβ”€β”€ LoginPage.tsx
β”‚           β”‚   β”œβ”€β”€ RegisterPage.tsx      # Plan selection + coupon
β”‚           β”‚   β”œβ”€β”€ DashboardPage.tsx     # "Your Events"
β”‚           β”‚   β”œβ”€β”€ NewWeddingPage.tsx    # 4-step wizard, 70+ event types / 8 communities
β”‚           β”‚   β”œβ”€β”€ WeddingEditorPage.tsx # 6-tab editor
β”‚           β”‚   β”œβ”€β”€ CardMakerPage.tsx     # DOCX invitation card generator
β”‚           β”‚   β”œβ”€β”€ TemplatesPage.tsx
β”‚           β”‚   β”œβ”€β”€ AnalyticsPage.tsx
β”‚           β”‚   β”œβ”€β”€ AccountPage.tsx       # Plan upgrade + coupon
β”‚           β”‚   └── AdminPanelPage.tsx    # Super-admin: Events, Users, Gallery, Plan Config
β”‚           β”œβ”€β”€ components/
β”‚           β”‚   β”œβ”€β”€ layout/AdminLayout.tsx
β”‚           β”‚   └── editor/
β”‚           β”‚       β”œβ”€β”€ DesignTab.tsx
β”‚           β”‚       β”œβ”€β”€ ContentTab.tsx        # Gift registry/items, love-story timeline, vendor credits
β”‚           β”‚       β”œβ”€β”€ GuestsTab.tsx
β”‚           β”‚       β”œβ”€β”€ EventsTab.tsx
β”‚           β”‚       β”œβ”€β”€ MediaTab.tsx
β”‚           β”‚       β”œβ”€β”€ AnalyticsTab.tsx
β”‚           β”‚       β”œβ”€β”€ SettingsTab.tsx
β”‚           β”‚       └── AIStoryWriterModal.tsx # Local phrase-bank story generator UI
β”‚           β”œβ”€β”€ lib/storyGenerator.ts     # On-device "AI" story drafts β€” no external API call
β”‚           β”œβ”€β”€ vite-env.d.ts             # VITE_* env vars typed (v54)
β”‚           └── stores/auth.ts            # Zustand JWT store
β”‚
β”œβ”€β”€ workers/
β”‚   └── api/              # Cloudflare Worker β€” Hono REST API β€” v123.0.0
β”‚       └── src/
β”‚           β”œβ”€β”€ index.ts              # Main Hono router
β”‚           β”œβ”€β”€ middleware/auth.ts    # JWT verification middleware (stateless β€” no DB hit)
β”‚           β”œβ”€β”€ lib/planConfig.ts     # Two-tier (in-memory + KV) cached plan limits
β”‚           └── routes/
β”‚               β”œβ”€β”€ auth.ts           # Register, login, me, forgot/reset-password, setup-admin
β”‚               β”œβ”€β”€ weddings.ts       # CRUD + KV cache invalidation (WEDDING_CACHE_VERSION)
β”‚               β”œβ”€β”€ guests.ts         # Guest list + batched CSV bulk import
β”‚               β”œβ”€β”€ rsvp.ts           # Public RSVP (no auth required)
β”‚               β”œβ”€β”€ media.ts          # Cloudinary upload/delete/reorder (batched)
β”‚               β”œβ”€β”€ gallery.ts        # Platform-wide real-wedding gallery (admin + public)
β”‚               β”œβ”€β”€ checkin.ts        # Guest arrival check-in + live day-of updates
β”‚               β”œβ”€β”€ public.ts         # Public event data (cached in KV + Cloudflare Cache API)
β”‚               β”œβ”€β”€ payments.ts       # Razorpay + coupon validation
β”‚               β”œβ”€β”€ admin.ts          # Super-admin endpoints
β”‚               β”œβ”€β”€ analytics.ts      # Page view tracking (batched aggregate queries)
β”‚               β”œβ”€β”€ guestbook.ts      # Guestbook CRUD + moderation
β”‚               β”œβ”€β”€ events.ts         # Sub-events incl. corporate (keynote, workshop…)
β”‚               β”œβ”€β”€ whatsapp.ts       # Free wa.me link queue + sent/delivered tracking
β”‚               β”œβ”€β”€ email.ts          # MSG91 bulk guest-facing email invites
β”‚               β”œβ”€β”€ audio.ts          # Background-music streaming proxy
β”‚               β”œβ”€β”€ expiry.ts         # Cron: auto-expire pages + telemetry pruning
β”‚               └── og.ts             # OG image generation (resvg-wasm), 7-day Cache API TTL
β”‚
β”œβ”€β”€ schema/
β”‚   β”œβ”€β”€ 000_full_schema.sql          # Canonical full schema (fresh install)
β”‚   β”œβ”€β”€ 002_seed.sql                 # 80+ templates across all communities
β”‚   └── 001–039_*.sql                # Incremental migrations v1β†’v123
β”‚
β”œβ”€β”€ scripts/
β”‚   β”œβ”€β”€ deploy.sh                    # Full deploy orchestration
β”‚   β”œβ”€β”€ migrate.sh                   # DB migration runner
β”‚   └── set-secrets.sh               # Interactive secret setup
β”‚
β”œβ”€β”€ tests/
β”‚   β”œβ”€β”€ test_einvite.py              # Production readiness suite
β”‚   β”œβ”€β”€ comprehensive_test.py
β”‚   β”œβ”€β”€ test_production_v67.py
β”‚   └── regression-v92.test.js
β”‚
β”œβ”€β”€ CHANGELOG-V*.md                  # Per-release notes (see Release Highlights)
β”œβ”€β”€ wrangler.toml                    # Root config (reference)
└── pnpm-workspace.yaml

πŸ—„οΈ Database Schema

D1 (SQLite-compatible) running on Cloudflare. Schema is defined in schema/000_full_schema.sql with 39 incremental migration files for upgrades (v1 β†’ v123).

TablePurposeKey Fields
usersAccount holders, admins, event plannersid, email, password_hash (PBKDF2), role, plan_tier, plan_expires_at
weddingsOne invitation page per event (table name retained for backward compatibility)id, user_id, slug, template_id, religion, event_type, status, plan_tier, page_expires_at, dresscode, custom_message, background_music_url, hero_video_url, gift_registry_url/label, gift_items (JSON), love_story_timeline (JSON), vendor_credits (JSON), live_update_text/at, save_date_enabled, qr_code_url, og_image_url, special_notes
eventsSub-events / function schedule within an invitation, incl. corporate (keynote, workshop, networking, awards_ceremony, team_activity)id, wedding_id, event_name, event_type, event_date, event_time, venue_name, dress_code, notes, sort_order
guestsGuest list with RSVP + arrival stateid, wedding_id, name, phone, invite_token, rsvp_status, guest_count, arrived_at, whatsapp_status, invite_email_sent_at
guest_checkinsArrival log for the guest check-in tool NEW v100id, wedding_id, guest_id, guest_name, checked_in_at, notes
guestbook_entriesPublic guestbook messagesid, wedding_id, name, message, emoji, is_approved
mediaCloudinary-backed photos/videos/music per eventid, wedding_id, public_url, thumbnail_url, type, category, sort_order, approved, is_featured
platform_galleryCurated cross-platform real-wedding gallery shown on the landing page NEW v81id, title, media_url, media_type, sort_order, is_active
page_viewsAnalytics hitsid, wedding_id, viewed_at, country, city
whatsapp_messageswa.me send/delivery log (no Meta API involved since v68)id, wedding_id, type, status, created_at
paymentsRazorpay order recordsid, user_id, razorpay_order_id, amount, status, plan_key
upgrade_transactionsPlan upgrade history + coupon trackingid, user_id, from_plan, to_plan, coupon_code, discount_pct
plan_configDB-driven plan definitions (single source of truth for limits, two-tier cached)key, name, price_inr, max_weddings, max_guests, max_photos, expiry_days, whatsapp/analytics/video/custom_domain flags
templates80+ invitation templates incl. corporate designsid, name, religion, event_type, config (JSON theme), is_premium, sort_order
discount_codesReferral coupons for plannersid, code (6-char), discount_pct, created_by, use_count, is_active
app_configSystem settingskey, value (schema_version, expiry_warning_days)
audit_logAdmin action trail (pruned after 180 days)id, actor_id, action, resource_type, resource_id, created_at
ℹ️
"Weddings" β†’ "Events" β€” naming note (v61) As of v61, all user-facing UI text and API error messages use "event"/"events" instead of "wedding"/"weddings" (e.g. "Event not found", "Your Events", "3 event(s)"). The underlying database table weddings, column max_weddings, field wedding_id, and the API route prefix /weddings are unchanged for backward compatibility β€” this is purely a display-layer rename.
⚠️
Never run pnpm db:fresh on a production database. This command drops all tables and recreates them. It is only safe for brand-new installs. For existing databases, always use pnpm db:migrate:remote.

🌍 Event Communities & Types

eInvit covers 8 community groups and 70+ event types, defined in apps/admin/src/pages/NewWeddingPage.tsx (EVENT_TYPES map) and mirrored in the public landing page's community grid.

Communityreligion valueEvent TypesTemplates
✝️ Christianchristianwedding, betrothal, engagement, first_holy_communion, baptism, reception, birthday, housewarming, anniversary, graduation, sadya, other (12)8+
πŸ•‰οΈ Hinduhinduwedding, engagement, naming_ceremony, vidyarambham, upanayanam, seemantham, sadya, housewarming, birthday, anniversary, graduation, reception, other (13)8+
β˜ͺ️ Muslimmuslimnikah, walima, engagement, aqiqah, birthday, housewarming, graduation, anniversary, reception, sadya, other (11)6+
🏯 North Indiannorth_indianShaadi / Vivah, Roka Ceremony, Sagai (Engagement), Sangeet Night, Mehendi Ceremony, Haldi Ceremony, Reception, Mundan (8)8+
🌴 South Indiansouth_indianTamil Kalyanam, Telugu Pellikoduku, Kannada Maduve, Nischayathartham, Seemantham, Ayushya Homam, Puberty Ceremony, Housewarming (8)8+
🌏 International / NRIinternationalDestination Wedding, NRI Wedding Reception, Engagement Party, Bridal Shower, Rehearsal Dinner, Wedding Anniversary, Vow Renewal, Cultural Fusion (8)6+
πŸ’ Interfaith / Otherinterfaith / otherwedding, engagement, reception, anniversary, birthday, housewarming, graduation, other (7)7+
🏒 Corporate Eventsother (event_type prefixed corp_)Product Launch, Conference / Summit, Annual Day / Company Anniversary, Award Ceremony, Team Offsite / Retreat (5)7+ NEW v93

Corporate events also expand the events (sub-event) table's allowed types with keynote, workshop, networking, awards_ceremony, and team_activity (migration 029_v93_corporate_event_types.sql), so a single conference invitation page can carry a full multi-track agenda alongside the usual ceremony/reception schedule.

The single_person flag on an event type (e.g. baptism, birthday, vidyarambham, every corporate type) tells the wizard and editor to render a single-name/host field set instead of the two-person (couple) field set β€” used by getPersonLabel() and isSinglePerson() in NewWeddingPage.tsx.

Event-Specific Templates

Beyond the base community templates, getTemplates() in NewWeddingPage.tsx returns extra event-specific designs layered on top of the community base set β€” e.g. christian-fhc-white-dove for First Holy Communion, muslim-nikah-gold-moon for Nikah, hindu-upanayanam-saffron for Upanayanam, generic-sadya-banana for Sadya/Feast across any community, and corporate-conference-horizon / corporate-launch-ignite / corporate-awards-spotlight for the Corporate community.

πŸ’³ Plan Configuration

All plan limits are stored in the plan_config table and read via workers/api/src/lib/planConfig.ts β€” there are no hardcoded plan limits in route handlers (since v31). Reads are two-tier cached (in-memory 5 min, KV 10 min) since v71, so this ~6-row table is almost never hit directly on the request hot path. The current values (last synced to the live database in v95, migration 032_v95_plan_config_sync.sql):

PlankeyPrice (β‚Ή)max_weddings (events)max_guestsmax_photosexpiry_daysWhatsApp / Analytics / Video / Custom Domain
Freefree015 v6128 v61β€”
Basicbasic699110010365 v95β€”
Premiumpremium999230040545 v95βœ“ / βœ“ / β€” / β€”
Premium+premium_plus1,4993 v61500100730 v95βœ“ / βœ“ / βœ“ / β€”
Ultimateultimate1,9998 v951,000100βˆ’1 (never)βœ“ / βœ“ / βœ“ / βœ“

To apply these values to a database that has not yet run the v95 sync (e.g. an older fork), run:

wrangler d1 execute TAL_DB --remote --file=schema/032_v95_plan_config_sync.sql

# Verify
wrangler d1 execute TAL_DB --remote --command="SELECT key, max_weddings, max_guests, expiry_days FROM plan_config ORDER BY sort_order"

Relevant environment variable:

# workers/api/wrangler.toml and root wrangler.toml [vars]
MAX_FREE_GUESTS = "5"   # mirrors plan_config.free.max_guests

⚑ Performance & Caching

eInvit runs entirely on Cloudflare's free tier, so every request that avoids a D1 round-trip or a cold Worker invocation directly protects the daily quota. The codebase has been through several dedicated performance passes (most notably v71); this section documents the current state, including a few gaps closed in this latest audit.

Caching layers, fastest to slowest

LayerUsed forTTL
In-memory (per Worker isolate)plan_config rows (planConfig.ts)5 min
Cloudflare KVplan_config (L2), public templates, wedding-slug sitemap list, demo list, public invitation pages (/public/w/:slug)10–30 min (5 min for personalised-free pages)
Cloudflare Cache API (CDN edge)OG preview images (/og/w/:slug) β€” rasterised once, served from the edge for every social-media crawler hit afterwards7 days, stale-while-revalidate 1 day
HTTP Cache-Control (edge, no Worker invocation at all)/public/event-types β€” fully static payload NEW1 hr browser / 1 day edge, stale-while-revalidate 7 days

D1 batching & parallelism

D1 charges a quota unit per query, and each await on a separate .prepare().run() is a full network round-trip from the Worker to D1. The codebase consistently uses env.DB.batch([...]) to ship multiple statements in one round-trip, and Promise.all to parallelise independent reads:

  • GET /public/w/:slug β€” wedding existence-check + full row fetch run as one D1 batch on a cache miss; events/media/guestbook are then fetched in parallel via Promise.all; the view-count increment and page_views insert are batched together and run as a non-blocking background task via waitUntil so they never add latency to the response.
  • GET /analytics/overview and GET /analytics/:weddingId β€” every aggregate query batched into a single D1 round-trip.
  • GET /admin/stats β€” six independent COUNT/GROUP BY queries run with Promise.all.
  • PATCH /media/reorder, checkin.ts arrival/list endpoints β€” already batched.
πŸ”§
Fixed in this audit Two write paths still issued one sequential D1 round-trip per row instead of a single batch β€” POST /guests/:weddingId/bulk (CSV import, previously up to 1,000 sequential writes for an Ultimate-plan import) and POST /gallery/reorder (platform gallery drag-and-drop, missed when the identical fix shipped for /media/reorder). Both now use env.DB.batch() β€” one round-trip regardless of row count. GET /public/event-types also gained a long-lived Cache-Control header since its payload is fully static and was previously rebuilt and shipped from the Worker on every single request.

Other deliberate performance choices

  • Stateless auth β€” JWT verification is pure Web Crypto (HMAC-SHA256), so authenticated requests never touch D1 or KV just to check who's calling. Only login/register pay the PBKDF2 (100,000 iterations) cost.
  • Lazy WASM singleton β€” the @resvg/resvg-wasm OG-image renderer initialises once per Worker isolate (module-scope promise), not per request.
  • Cache busting is explicit and targeted β€” WEDDING_CACHE_VERSION bump + bustPlanConfigCache() invalidate only the KV keys that changed, rather than flushing broadly.
  • Telemetry pruning β€” the daily cron (expiry.ts) deletes page_views/whatsapp_messages older than 90 days and audit_log older than 180 days in the same D1 batch as the expiry job, keeping D1's free-tier row budget in check as the platform grows.
  • Composite, query-shaped indexes β€” e.g. idx_weddings_expiry_cron (partial index for the cron's exact WHERE clause), idx_guests_rsvp on (wedding_id, rsvp_status), idx_page_views_wedding_time on (wedding_id, viewed_at DESC) β€” every hot-path query in analytics.ts, public.ts, and checkin.ts is covered by an index shaped for its actual WHERE/ORDER BY.
πŸ“Œ
Known, accepted trade-off /admin/users and /admin/weddings search uses LIKE '%term%', which can't use an index for the leading wildcard. At the platform's current data volume this is fine (paginated, admin-only, low traffic); if the users/weddings table grows into the hundreds of thousands of rows, an SQLite FTS5 virtual table would be the next step rather than further D1 batching.

πŸ“‹ Prerequisites

RequirementVersionInstall
Node.jsβ‰₯ 20.0.0nodejs.org
pnpmβ‰₯ 9.0.0npm install -g pnpm
Wrangler CLIβ‰₯ 4.40.0npm install -g wrangler
Cloudflare accountFree tiercloudflare.com
Cloudinary accountFree tiercloudinary.com
Razorpay accountβ€”razorpay.com
Resend accountFree tierresend.com β€” password reset & expiry emails only
MSG91 accountFree tiermsg91.com β€” bulk guest-facing email invites

No Meta Business / WhatsApp Business API account is needed. WhatsApp delivery uses free wa.me links (since v68) β€” nothing to register or get approved.

πŸ†• Fresh Installation

Step 1 β€” Clone & Install

git clone https://github.com/your-org/einvite.git
cd einvite
pnpm install

Step 2 β€” Authenticate with Cloudflare

wrangler login
# Opens browser β†’ log in β†’ authorise Wrangler

Step 3 β€” Create Cloudflare Resources

# Create D1 SQLite database
wrangler d1 create TAL_DB
# β†’ Copy the database_id from output

# Create KV namespaces
wrangler kv:namespace create TAL_SESSIONS
wrangler kv:namespace create TAL_CACHE
# β†’ Copy the id values from each output

Step 4 β€” Update wrangler.toml

Edit workers/api/wrangler.toml and replace placeholder IDs:

[[d1_databases]]
binding       = "DB"
database_name = "TAL_DB"
database_id   = "YOUR_ACTUAL_D1_ID"  # from Step 3

[[kv_namespaces]]
binding = "SESSIONS"
id      = "YOUR_ACTUAL_KV_SESSIONS_ID"

[[kv_namespaces]]
binding = "CACHE"
id      = "YOUR_ACTUAL_KV_CACHE_ID"

[vars]
ENVIRONMENT           = "production"
APP_URL               = "https://einvit.in"
ADMIN_URL             = "https://admin.einvit.in"
CLOUDINARY_CLOUD_NAME = "your-cloudinary-cloud-name"
MAX_FREE_GUESTS       = "5"

Step 5 β€” Set Secrets

Secrets are stored encrypted in Cloudflare and never appear in your code or config files.

cd workers/api

# Auth
wrangler secret put JWT_SECRET              # 64-char random hex (openssl rand -hex 32)
wrangler secret put ADMIN_BOOTSTRAP_PASSWORD
wrangler secret put ADMIN_SETUP_KEY

# Media
wrangler secret put CLOUDINARY_API_KEY
wrangler secret put CLOUDINARY_API_SECRET

# Payments
wrangler secret put RAZORPAY_KEY_ID
wrangler secret put RAZORPAY_KEY_SECRET
wrangler secret put RAZORPAY_WEBHOOK_SECRET

# Bulk guest email (optional β€” only needed for the email-invite feature)
wrangler secret put MSG91_AUTH_KEY

# Transactional email β€” password reset, expiry warnings
wrangler secret put RESEND_API_KEY

cd ../..
πŸ’‘
Use the helper script bash scripts/set-secrets.sh for an interactive, prompted setup that handles Cloudinary auto-detection. WhatsApp delivery needs no secret at all β€” since v68 it sends free wa.me click-to-chat links instead of calling the paid Meta Cloud API, so there's nothing to provision or get approved.

Step 6 β€” Initialize Database

# Full schema (fresh install only β€” drops & recreates all tables)
pnpm db:fresh

# Seed templates and demo data (includes North/South Indian, International + Corporate templates)
wrangler d1 execute TAL_DB --remote --file=schema/002_seed.sql

# Optional: load 15 curated demo invitations spanning every community and plan tier
wrangler d1 execute TAL_DB --remote --file=schema/021_v59_demo_events.sql
wrangler d1 execute TAL_DB --remote --file=schema/039_v123_demo_events_enrichment.sql

Step 7 β€” Deploy All Packages

# One command deploys API Worker + Admin SPA + Public Astro site
pnpm deploy:code

Step 8 β€” Bootstrap Admin Password

curl -X POST https://api.einvit.in/auth/setup-admin \
  -H "Content-Type: application/json" \
  -d '{"setup_key":"<ADMIN_SETUP_KEY>"}'

# Admin login: admin@einvite.app / <ADMIN_BOOTSTRAP_PASSWORD>

Step 9 β€” Configure Custom Domains

In Cloudflare Dashboard β†’ Pages:

# einvite-public β†’ Custom Domains
einvit.in
www.einvit.in

# einvite-admin β†’ Custom Domains
admin.einvit.in

# Workers & Pages β†’ Routes
api.einvit.in/* β†’ einvite-api (Worker)

πŸ”„ Upgrade Guide

Upgrading to v139 / v140 (Recommended β€” Full Deploy)

# Safest: runs all pending migrations + redeploys all three packages
pnpm deploy

Upgrading from an Install Older than v123

Migrations are numbered sequentially and are safe to re-run (CREATE TABLE IF NOT EXISTS, INSERT OR IGNORE, idempotent UPDATEs) β€” just run everything newer than your current schema_version. The highest-impact ones if you're upgrading from anywhere before v90:

MigrationVersionSummary
029_v93_corporate_event_types.sqlv93Adds the Corporate Events community's sub-event types (keynote, workshop, networking, awards_ceremony, team_activity)
030_v93b_corporate_templates.sqlv937 corporate-themed templates (Product Launch, Conference, Annual Day, Award Ceremony, Team Offsite)
032_v95_plan_config_sync.sqlv95Syncs plan expiry days & Ultimate's max_weddings to the values that had drifted into the live DB via admin-panel edits
033_v100_new_features.sqlv100Gift registry link, hero video, love-story timeline, vendor credits, live day-of updates, save-the-date toggle, QR code, and the guest_checkins table
038_v122_gift_items.sqlv122Itemised Gift Wish List (weddings.gift_items JSON column)
039_v123_demo_events_enrichment.sqlv123Enriches all 15 demo invitations with v100+ features; adds 3 new demos (Corporate, Naming Ceremony, Birthday)
040_v133_payment_safety.sqlv133Payment safety hardening
041_v135_unified_expiry.sqlv135Sets weddings.page_expires_at = users.plan_expires_at for all active paid plans β€” unified expiry
042_v137_max_videos_plan_config.sqlv137Adds max_videos column to plan_config
043_v140_event_types_expand.sqlv140Expands events.event_type CHECK constraint to 43 valid values (28 new types)
044_v140_new_templates.sqlv140Seeds 15 new templates to templates table
039_v123_demo_events_enrichment.sqlv123Brings demo invitations to full feature parity and adds 3 new ones (15 total)
pnpm db:migrate:remote   # runs every pending migration in order
pnpm deploy:code         # redeploy API + Admin + Public

Selective Upgrades

# DB migrations only (no code change)
pnpm deploy:db

# Code only (no DB changes)
pnpm deploy:code

# Individual packages
pnpm deploy:api      # Cloudflare Worker only
pnpm deploy:admin    # Admin SPA only
pnpm deploy:public   # Public Astro SSR only

Check Schema Version

pnpm db:version
# Should return: 123

πŸš€ Deploy Command Reference

CommandAction
pnpm deployFull deploy: DB migrations β†’ API β†’ Admin β†’ Public
pnpm deploy:dbDB migrations only
pnpm deploy:codeAll three packages, no DB
pnpm deploy:apiAPI Cloudflare Worker only
pnpm deploy:adminAdmin SPA only (Cloudflare Pages)
pnpm deploy:publicPublic Astro SSR only (Cloudflare Pages)
pnpm db:fresh⚠️ Drop & recreate all tables (fresh install only)
pnpm db:migrate:remoteRun all 39 incremental migrations on production
pnpm db:versionPrint current schema version from production DB
pnpm admin:resetReset admin password back to PBKDF2_PLACEHOLDER
pnpm secrets:setInteractive secret setup helper
pnpm typecheckRun TypeScript checks across all packages

βœ… Post-Deploy Health Checks

# 1. API health
curl https://api.einvit.in/health
# Expected: {"status":"ok","version":"123.0.0"}

# 2. Plan config (must return 5 plans with current limits)
curl "https://api.einvit.in/payments/plans?show_public=1" | jq '.plans | length'
# Expected: 5
curl "https://api.einvit.in/payments/plans?show_public=1" | jq '.plans[] | {key, max_guests, max_weddings, expiry_days}'
# free.max_guests = 5, free.expiry_days = 8
# premium.expiry_days = 545, premium_plus.expiry_days = 730
# ultimate.max_weddings = 8, ultimate.expiry_days = -1

# 3. Schema version
pnpm db:version
# Expected: 123

# 4. Demo invitation page β€” note the "demo-" slug prefix
curl -s https://einvit.in/w/demo-riya-rahul-2026 | head -5
# Expected: valid HTML

# 5. Admin login
open https://admin.einvit.in
# demo@einvite.app / TalDemo@2026

# 6. Corporate community spot-check
# Landing page community grid should show 8 communities incl. "Corporate Events"
# New Event wizard β†’ Corporate β†’ should list 5 event types incl. "Conference / Summit"

# 7. Free WhatsApp delivery (no API key needed)
# Editor β†’ Guests β†’ "Send via WhatsApp" should open a wa.me link, not call any Meta API

🌐 API Reference

Base URL: https://api.einvit.in. All authenticated endpoints require Authorization: Bearer <jwt>.

πŸ”
JWT tokens are issued on login with HMAC-SHA256 signing using the JWT_SECRET. Tokens expire after 7 days. Passwords are hashed with PBKDF2 at 100,000 iterations using the Web Crypto API β€” no external libraries.

Auth Endpoints

POST/auth/registerCreate account + plan
Body: { email, password, plan_key, coupon_code? } β†’ Returns { token, user }. user.plan_expires_at included since v53.
POST/auth/loginAuthenticate
Body: { email, password } β†’ Returns { token, user }
GET/auth/meCurrent user info πŸ”
Returns { user: { id, email, role, plan, plan_expires_at } } in a single query (merged since v46).
POST/auth/setup-adminBootstrap admin password
Body: { setup_key }. One-time endpoint. Sets admin@einvite.app password to ADMIN_BOOTSTRAP_PASSWORD secret.

Event Endpoints

Route prefix remains /weddings for backward compatibility β€” only the JSON error messages and UI labels say "event" (v61).

GET/weddingsList user's events πŸ”
Returns array of event/invitation objects for the authenticated user.
POST/weddingsCreate event πŸ”
Body: { title, slug, bride_name, groom_name, ceremony_date, venue_name, template_id, religion, event_type, ... }. religion now accepts north_indian, south_indian, and international in addition to christian/hindu/muslim/interfaith/other. Returns 403 with "Your {plan} plan allows {N} event(s). Upgrade to create more." if the plan's max_weddings limit is reached.
PUT/weddings/:idUpdate event πŸ”
Partial update. Invalidates KV cache on save via bustWeddingCache (uses WEDDING_CACHE_VERSION constant). Returns 404 { error: "Event not found" } if not owned by caller.
GET/public/w/:slugPublic event data (no auth)
Returns all public invitation data. Response is cached in KV for 5 minutes. Cache key: wedding:slug:{WEDDING_CACHE_VERSION}

Guests & RSVP

GET/guests?wedding_id=:idList guests πŸ”
POST/guests/:weddingId/bulkCSV bulk import πŸ”
Body: { guests: [{name, phone?, email?, relation?, side?, category?}] }. Limit check joins to users.plan_tier β€” returns inserted/skipped counts plus up to 10 error messages. Inserted as a single env.DB.batch() call regardless of row count (fixed in this performance pass β€” previously one D1 round-trip per row).
POST/rsvp/:tokenPublic RSVP (no auth)
Body: { status: 'yes'|'no'|'maybe', meal_pref?, plus_count?, message? }. Token is unique per guest. Returns 404 { error: "Event not found or not accepting RSVPs" } for closed/missing events.

Media & Gallery

POST/media/uploadUpload to Cloudinary πŸ”
Multipart form-data: file, wedding_id, media_type (photo|video|music). Server signs Cloudinary upload request. File guard uses instanceof File with duck-type fallback (v54). Limit reads from users.plan_tier.
DELETE/media/:idDelete media πŸ”
Deletes from Cloudinary and removes DB record.
PATCH/media/reorderReorder per-event media πŸ”
Body: { items: [{id, sort_order}] }. Batched into one D1 round-trip.
GET/galleryCurated platform gallery (public)
Real-wedding showcase shown on the landing page, independent of any single event's own media. NEW v81
POST/gallery/reorderReorder platform gallery πŸ”πŸ‘‘
Body: { items: [{id, sort_order}] }. Batched into one D1 round-trip (fixed in this performance pass to match /media/reorder).

Check-in & Live Updates

POST/checkin/:weddingIdMark a guest as arrived πŸ”
Body: { guest_id?, guest_name, notes? }. Works for walk-ins without a pre-existing guest row too. NEW v100
GET/checkin/:weddingIdList arrivals πŸ”
PUT/weddings/:id/live-updatePost a day-of live update πŸ”
Sets live_update_text + live_update_at, shown as a banner on the public invitation page β€” e.g. "Ceremony running 15 min late."

Payments

GET/payments/plansList plans (public)
Query: ?show_public=1 to exclude internal planner plan. Returns DB-driven plan_config rows with the current limits (free guests=5/expiry=8, premium expiry=545, premium_plus expiry=730, ultimate events=8/expiry=never). Feature list renders as "Unlimited events" or "N event(s)".
POST/payments/create-orderCreate Razorpay order πŸ”
Body: { plan_key, coupon_code? }. Server validates coupon server-side, creates Razorpay order, returns order_id.
POST/payments/verifyVerify payment πŸ”
Body: { razorpay_payment_id, razorpay_order_id, razorpay_signature }. Verifies HMAC, upgrades user plan, syncs all active events to the new plan's expiry_days from plan_config.
POST/payments/webhookRazorpay webhook
Handles payment.captured and payment.failed events. Validates webhook HMAC signature using RAZORPAY_WEBHOOK_SECRET (typed since v54).

Admin (Super-Admin Only)

GET/admin/usersList all users πŸ”πŸ‘‘
PUT/admin/users/:id/planChange user plan πŸ”πŸ‘‘
GET/admin/discount-codesList coupons πŸ”πŸ‘‘
POST/admin/discount-codesCreate coupon πŸ”πŸ‘‘
Body: { code: "SAVE20", discount_pct: 20 }. Code is 6 chars; last 2 digits encode discount %.
GET/admin/weddings/:idGet any event πŸ”πŸ‘‘
Admin override β€” returns 404 { error: "Event not found" } if missing.
POST/admin/run-expiryManually trigger expiry job πŸ”πŸ‘‘
Returns { expired, warned, plans_expired } (full response since v53).

☁️ Cloudinary Integration

eInvit uses Cloudinary free tier for all photo, video, and music uploads. The server signs upload requests β€” API keys are never sent to the browser.

# Environment variables required
CLOUDINARY_CLOUD_NAME = "your-cloud-name"   # in wrangler.toml [vars]
CLOUDINARY_API_KEY    = "..."               # secret
CLOUDINARY_API_SECRET = "..."               # secret

Upload flow: Client calls POST /media/upload β†’ Worker generates a signed Cloudinary upload URL β†’ Client uploads directly to Cloudinary β†’ Cloudinary URL stored in D1.

πŸ’³ Razorpay Integration

All Indian payment methods supported: UPI, NetBanking, Debit/Credit Cards, Wallets.

RAZORPAY_KEY_ID      = "rzp_live_..."
RAZORPAY_KEY_SECRET  = "..."
RAZORPAY_WEBHOOK_SECRET = "..."  # Set in Razorpay Dashboard β†’ Webhooks

Payment flow: Client requests order β†’ Worker creates Razorpay order β†’ Client shows Razorpay checkout β†’ On success, Worker verifies HMAC signature β†’ Plan upgraded in D1 β†’ active events' page_expires_at synced to the new plan's expiry_days.

πŸ’¬ WhatsApp Delivery (wa.me)

πŸ†“
No API, no approval, no cost. Since v68, eInvit no longer uses the paid Meta Cloud API / WABA. Instead, routes/whatsapp.ts builds a pre-filled wa.me click-to-chat link per guest β€” the host's own device opens WhatsApp with the invite text and link already typed in, and sends it from their own number. There is nothing to provision, no message templates to get approved by Meta, and no monthly conversation cap.

Flow: POST /whatsapp/:weddingId/send records the intended send (for the whatsapp_sent_count analytics counter and the per-guest dedup flag) and returns the wa.me/<phone>?text=<encoded message> URL; the admin UI opens it in a new tab/app. Bulk "Send to all pending" iterates guests client-side, opening one wa.me tab per guest with a short delay so the browser doesn't block pop-ups.

Since v53, explicit guest_ids sends respect the per-guest invite_sent_at dedup flag to prevent double-sends; this still applies under the wa.me flow.

πŸ“§ Email Delivery β€” Resend & MSG91

Two providers, split by audience:

ProviderUsed forWhy split
ResendPassword reset, plan-upgrade confirmation, page-expiry warnings β€” system/transactional mail to the hostReliable transactional delivery; generous enough free tier for low-volume system mail
MSG91Bulk "Email all guests" invite sends from the editor β€” guest-facing mail, can be hundreds of recipients per eventAdded in v68 because Resend's free tier wasn't enough headroom for bulk guest-facing sends on top of system mail
RESEND_API_KEY  = "re_..."
MSG91_AUTH_KEY  = "..."

# From address: einvit@proton.me (configure in both dashboards)

routes/email.ts handles the MSG91 bulk send β€” it chunks the guest list and tracks email_sent_count per event so the editor can show "120 of 300 guests emailed."

⏰ Cron Jobs

Defined in workers/api/wrangler.toml:

[triggers]
crons = ["0 2 * * *"]   # Daily at 02:00 UTC

The daily cron runs routes/expiry.ts which:

  • Finds all weddings/events where page_expires_at < NOW(), computed from plan_config.expiry_days for the user's plan (Free=8, Basic=365, Premium=545, Premium+=730, Ultimate=βˆ’1/never)
  • Sets status = 'expired' on expired pages
  • Logs the expiry count to audit_log
  • Sends warning emails (via Resend) N days before expiry (configurable via app_config.expiry_warning_days)
  • Prunes telemetry in the same run β€” deletes page_views and whatsapp_messages rows older than 90 days and audit_log rows older than 180 days, keeping D1's free-tier row count from growing unbounded as the platform scales
  • Reads RESEND_API_KEY and APP_URL directly from env (typed, no casts since v54)

πŸ—ƒοΈ Database Migrations

FileVersionChanges
000_full_schema.sqlCanonicalComplete schema β€” fresh installs only (baseline only; run 023–039 after for a fully current DB)
002_seed.sqlβ€”80+ templates across all 8 communities incl. Corporate + demo data
009_v31_plan_config_driven.sqlv31DB-driven plan_config table
010_v32_sort_order_show_public.sqlv32Template sort_order + show_public flag
011_v34_production_fixes.sqlv34Production data fixes
012_v35_audit_fixes.sqlv35Audit log improvements
014_v41_event_description.sqlv41Event description field
015_v43_production_fixes.sqlv43Production index + constraint fixes
016_v46_auth_query_fix.sqlv46Auth query performance fix (marker only)
017_v48_discount_codes.sqlv48Coupon system tables (discount_codes)
018_v50_coupon_fixes.sqlv50Coupon validation fixes
019_v51_content_music_fields.sqlv51Music + content block fields (dresscode, custom_message, etc.)
020_v54_type_safety.sqlv54Marker migration β€” type safety release, no schema changes
021_v59_demo_events.sqlv5912 curated demo invitations covering all communities
022_v61_plan_tier_updates.sqlv61Free guests 10β†’5 & expiry 10β†’8 days; Premium expiry 90β†’180; Premium+ events 2β†’3 & expiry 90β†’270; "Weddings"β†’"Events" rebrand
023_v65_icon_version_bump.sqlv65PWA icon/splash-screen refresh β€” marker only, no schema change
024_v67_ultimate_never_expires.sqlv67Ultimate plan's expiry_days set to βˆ’1 (never expires)
025_v68_responses_email_wa.sqlv68KEY WhatsApp moved from paid Meta Cloud API to free wa.me links; adds MSG91 bulk-email columns (email_sent_count) and the cross-event Responses dashboard
026_v69_features_page_update.sqlv69Marketing/content-only β€” homepage features section rebuilt, no schema change
027_v71_performance.sqlv71KEY Major performance pass β€” composite covering indexes, D1 query batching, two-tier plan_config caching
028_v81_platform_gallery.sqlv81platform_gallery table β€” curated real-wedding showcase for the landing page
029_v93_corporate_event_types.sqlv93KEY Corporate Events community's sub-event types (keynote, workshop, networking, awards_ceremony, team_activity)
030_v93b_corporate_templates.sqlv937 corporate-themed templates
031_v94_schema_version.sqlv94Schema version bump marker
032_v95_plan_config_sync.sqlv95Synced plan_config expiry days & Ultimate's max_weddings to live values
033_v100_new_features.sqlv100KEY Gift registry, hero video, love-story timeline, vendor credits, live updates, save-the-date toggle, QR code, guest_checkins table
034_v101_css_fixes.sqlv101CSS fixes β€” marker only
035_v109_version_bump.sqlv109Version bump marker
036_v110_whatsapp_fixes.sqlv110WhatsApp reminder & personalised-queue endpoint additions (no schema change)
037_v113_version_bump.sqlv113Version bump marker
038_v122_gift_items.sqlv122Itemised Gift Wish List (weddings.gift_items JSON column)
039_v123_demo_events_enrichment.sqlv123NEW Brings all demo invitations to full feature parity, adds 3 new demos (15 total)
# Run all pending migrations (idempotent β€” safe to re-run)
pnpm db:migrate:remote

# Expected "safe" errors on existing DBs:
# "table already exists" β†’ already applied, skip
# "duplicate column name" β†’ column exists, skip

πŸ“ Release Highlights β€” v62 to v139 (v140)

77 versions shipped between v62 and v139. Rather than reproduce every per-version changelog file here (see the CHANGELOG-V*.md files in the repo root for the complete history), this section summarises the major, currently-live feature additions β€” i.e. what's actually still true about the app today.

ThemeShipped inWhat's true today
WhatsApp deliveryv68, v110Free wa.me click-to-chat links β€” no Meta API, no approval, no message cap. The Meta Cloud API / WABA integration described in older docs no longer exists in the codebase.
Bulk guest emailv68MSG91 Email API for guest-facing bulk sends, alongside Resend for system/transactional mail.
Performance passv71Composite covering indexes, D1 batch queries, and two-tier (in-memory + KV) caching for plan limits β€” see Performance & Caching.
Platform galleryv81A curated, cross-customer real-wedding showcase on the public landing page, managed from the admin panel.
Corporate Eventsv93Corporate community β€” Product Launch, Conference/Summit, Annual Day, Award Ceremony, Team Offsite, Seminar, Expo, Townhall (v140 added the last three) β€” with its own sub-event types and templates.
Rich content blocksv100Gift registry link, hero video (hero_video_url), "How We Met" love-story timeline (love_story_timeline JSON), vendor credits (vendor_credits JSON), live day-of updates (live_update_text), save-the-date mini-page, QR code, personalised WhatsApp invites, WhatsApp Reminder Blast.
Guest check-inv100A day-of arrival tracker β€” POST /checkin/:weddingId/arrive, walk-in support, animated progress bar. Now also accessible as a sub-tab in the Guests/Responses page (v139).
Card Maker Studiopre-v100, v140Client-side DOCX invitation-card generator. 214 templates at v122; expanded to 229 in v140 with 15 new designs across Christian, Hindu, Muslim, Universal, and Corporate categories.
AI Story Writerpre-v100On-device phrase-bank story generator (storyGenerator.ts). Template-based, not a call to an external LLM β€” zero cost, zero latency, works offline.
Gift Wish Listv122Itemised gift list (gift_items JSON column) β€” name, note, product link β€” as alternative/complement to gift_registry_url. Section renders when either is set.
Demo showcasev59, v12315 curated demo invitations spanning every community, plan tier, and the full v100+ feature set (including hero video, love-story timeline, vendor credits, gift items, live update).
HTML exportv126GET /weddings/:id/export-html and GET /weddings/:id/export-story-html β€” self-contained HTML download with branding watermark. Button in Admin β†’ Settings.
Content protectionv125, v128Client-side right-click, copy, print, DevTools keyboard-shortcut prevention. public-utils.js covers all public pages; admin-protect.js covers the admin SPA.
Registration UXv134Single-screen registration: account details and plan picker side-by-side. accountCreated flag prevents double-registration on payment retry. No setTimeout auto-navigation on payment failure.
Unified expiryv135users.plan_expires_at and weddings.page_expires_at now always equal purchase_timestamp + plan.expiry_days. computeUnifiedExpiresAt() in planConfig.ts is the single source of truth. Schema migration 041 back-fills existing rows.
API hardeningv136Global JSON body size guard (2 MB), slug length guard (120 chars), JSON field size cap in PATCH (64 KB), bulk import hard cap (2 000 guests), media reorder cap (500 items), coupon sanitisation, email length guard (254 chars), cron crash guard with try/catch.
Guests/Responses Hubv139ResponsesPage rebuilt with five sub-tabs: Guest List, Check-In, Reminders, Live Update, Personalised Invites. GuestsTab stripped of day-of panels (moved here). AdminLayout nav label updated to "Guests/Responses". No backend changes.
Event type expansionv14028 new event types added across all communities. Hindu event labels now use "English Name (RegionalName1 / RegionalName2)" format. Schema migration 043 re-creates the events.event_type CHECK constraint with all 43 valid values.
Regional language detectionv140Admin name fields detect IST timezone + browser locale subtag β€” shows Malayalam, Tamil, Telugu, Kannada, Hindi etc. for Indian users; hidden for international users. Detection runs on mount with no network call.
15 new templatesv1403 Christian + 3 Hindu + 3 Muslim + 3 Universal + 3 Corporate. Seeded via migration 044. Total: 229 templates in TemplatesPage.tsx BUILTIN_TEMPLATES pool.
Corporate template deep-linkv140"Use This Template" now passes ?template=<id>&event=<type>&religion=<val>&corp=1 β€” NewWeddingPage reads these on mount and jumps to Step 4 with everything pre-filled. Works for all religions.

For deprecated/historical context only: v54 removed all as any environment-variable casts; v48–v50 introduced the discount_codes coupon system; v58 added the North Indian, South Indian, and International/NRI communities. None of these required further action by v139.

πŸ”’ Security Architecture

πŸ”‘

JWT Auth

HMAC-SHA256 tokens signed with JWT_SECRET. 7-day expiry. Verified on every authenticated request.

πŸ”

Password Hashing

PBKDF2 with 100,000 iterations via Web Crypto API. No external bcrypt or argon2 dependencies.

πŸ›‘οΈ

Ownership Verification

Every authenticated mutation checks that the resource belongs to the requesting user before proceeding.

🌐

CORS

Restricted to known origins: einvit.in and admin.einvit.in. No wildcard CORS in production.

🏦

Server-Side Coupon Validation

Discount % is re-derived server-side from the coupon table β€” client-supplied values are never trusted.

⚑

DDoS Protection

Cloudflare's free plan includes automatic DDoS mitigation and rate limiting at the edge.

🧩

Type Safety (v54)

All env.* Worker bindings and import.meta.env.* variables are fully typed β€” no as any escape hatches in env access.

πŸ“‹

Plan Limit Enforcement (v53)

Guest, media, and event-count limits always read the live users.plan_tier via plan_config β€” never a stale snapshot.

πŸ’» Local Development

# Terminal 1 β€” API Worker (http://localhost:8787)
cd workers/api
wrangler dev --local --persist-to=../../.wrangler

# Terminal 2 β€” Public Astro site (http://localhost:4321)
pnpm dev:public

# Terminal 3 β€” Admin dashboard (http://localhost:5173)
pnpm dev:admin
πŸ’‘
The --persist-to=../../.wrangler flag makes local D1 and KV data persist between dev sessions. Without it, data resets on every wrangler dev restart.

πŸ§ͺ Testing

# Install Python dependencies
cd tests && pip install requests

# Run against production API
python test_einvite.py --url https://api.einvit.in

# Run against local dev
python test_einvite.py --url http://localhost:8787

The test suite covers: auth register/login, event CRUD, guest management, RSVP flow, media upload, payments, coupon validation, admin endpoints, and analytics. Additional suites: comprehensive_test.py (broader endpoint coverage), test_production_v67.py (post-v67 wa.me migration regression), and regression-v92.test.js (JS-based regression runner). After upgrading to v123, also manually verify: plan_config returns the current limits, the Corporate community appears in the wizard, and "Send via WhatsApp" opens a wa.me link rather than calling any Meta API.

πŸ”‘ Demo Credentials

RoleEmailPassword
Demo coupledemo@einvite.appTalDemo@2026
Super adminadmin@einvite.appADMIN_BOOTSTRAP_PASSWORD secret
βœ…
Live Demo Invitation: einvit.in/w/demo-riya-rahul-2026 β€” opens without any login. Shows the full guest experience including countdown, gallery, RSVP, guestbook, gift wish list, and love-story timeline. 14 more demo invitations (v59, enriched in v123) covering every community β€” including Corporate (TechKochi Summit 2026) β€” and plan tier are admin-visible via demo-* slugs.
πŸ’Œ
eInvit v123 β€” Designed & developed by Tony Augustine Labs (TAL)
Kerala, India Β· Powered entirely by Cloudflare's free stack Β· Β© 2026