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.
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.
βοΈ Tech Stack
| Component | Technology | Purpose | Free Tier Limits |
|---|---|---|---|
| Public site | Astro 4 (SSR) + React Islands | Event invitation pages + landing | Unlimited static / Pages |
| Admin dashboard | React 18 + Vite + Zustand + TanStack Query | Event management SPA (installable PWA) | Unlimited / Pages |
| API | Cloudflare Worker + Hono 4 + Zod | REST backend | 100,000 req/day |
| Database | Cloudflare D1 (SQLite) | All app data | 5M row reads/day, 100K writes/day |
| Sessions & Cache | Cloudflare KV + in-memory (per-isolate) | JWT sessions, two-tier plan-config & page cache | 100K reads/day, 1K writes/day |
| Media | Cloudinary (free tier) | Photos, videos, music β CDN delivery | 25 credits/month |
| Payments | Razorpay | Indian payments (UPI, Cards, NetBanking) | 2% per transaction |
wa.me click-to-chat links | Personalised guest invitations β free, no WABA approval (since v68) | Unlimited, no API key | |
| Bulk Email | MSG91 Email API | Guest-facing bulk invite emails (since v68) | Larger free monthly quota |
| Transactional Email | Resend | Password reset, expiry warnings | 3,000 emails/month |
| OG Images | @resvg/resvg-wasm | Social preview images β CDN-cached 7 days via Cache API | Edge-rendered, no limit |
| Auth | HMAC-SHA256 JWT + PBKDF2 | Web Crypto API β no external deps | β |
| Monorepo | pnpm workspaces | Unified dependency management | β |
| TypeScript | 5.7+ | End-to-end type safety (fully typed since v54) | β |
| Wrangler | 4.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).
| Table | Purpose | Key Fields |
|---|---|---|
| users | Account holders, admins, event planners | id, email, password_hash (PBKDF2), role, plan_tier, plan_expires_at |
| weddings | One 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 |
| events | Sub-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 |
| guests | Guest list with RSVP + arrival state | id, wedding_id, name, phone, invite_token, rsvp_status, guest_count, arrived_at, whatsapp_status, invite_email_sent_at |
| guest_checkins | Arrival log for the guest check-in tool NEW v100 | id, wedding_id, guest_id, guest_name, checked_in_at, notes |
| guestbook_entries | Public guestbook messages | id, wedding_id, name, message, emoji, is_approved |
| media | Cloudinary-backed photos/videos/music per event | id, wedding_id, public_url, thumbnail_url, type, category, sort_order, approved, is_featured |
| platform_gallery | Curated cross-platform real-wedding gallery shown on the landing page NEW v81 | id, title, media_url, media_type, sort_order, is_active |
| page_views | Analytics hits | id, wedding_id, viewed_at, country, city |
| whatsapp_messages | wa.me send/delivery log (no Meta API involved since v68) | id, wedding_id, type, status, created_at |
| payments | Razorpay order records | id, user_id, razorpay_order_id, amount, status, plan_key |
| upgrade_transactions | Plan upgrade history + coupon tracking | id, user_id, from_plan, to_plan, coupon_code, discount_pct |
| plan_config | DB-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 |
| templates | 80+ invitation templates incl. corporate designs | id, name, religion, event_type, config (JSON theme), is_premium, sort_order |
| discount_codes | Referral coupons for planners | id, code (6-char), discount_pct, created_by, use_count, is_active |
| app_config | System settings | key, value (schema_version, expiry_warning_days) |
| audit_log | Admin action trail (pruned after 180 days) | id, actor_id, action, resource_type, resource_id, created_at |
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.
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.
| Community | religion value | Event Types | Templates |
|---|---|---|---|
| βοΈ Christian | christian | wedding, betrothal, engagement, first_holy_communion, baptism, reception, birthday, housewarming, anniversary, graduation, sadya, other (12) | 8+ |
| ποΈ Hindu | hindu | wedding, engagement, naming_ceremony, vidyarambham, upanayanam, seemantham, sadya, housewarming, birthday, anniversary, graduation, reception, other (13) | 8+ |
| βͺοΈ Muslim | muslim | nikah, walima, engagement, aqiqah, birthday, housewarming, graduation, anniversary, reception, sadya, other (11) | 6+ |
| π― North Indian | north_indian | Shaadi / Vivah, Roka Ceremony, Sagai (Engagement), Sangeet Night, Mehendi Ceremony, Haldi Ceremony, Reception, Mundan (8) | 8+ |
| π΄ South Indian | south_indian | Tamil Kalyanam, Telugu Pellikoduku, Kannada Maduve, Nischayathartham, Seemantham, Ayushya Homam, Puberty Ceremony, Housewarming (8) | 8+ |
| π International / NRI | international | Destination Wedding, NRI Wedding Reception, Engagement Party, Bridal Shower, Rehearsal Dinner, Wedding Anniversary, Vow Renewal, Cultural Fusion (8) | 6+ |
| π Interfaith / Other | interfaith / other | wedding, engagement, reception, anniversary, birthday, housewarming, graduation, other (7) | 7+ |
| π’ Corporate Events | other (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):
| Plan | key | Price (βΉ) | max_weddings (events) | max_guests | max_photos | expiry_days | WhatsApp / Analytics / Video / Custom Domain |
|---|---|---|---|---|---|---|---|
| Free | free | 0 | 1 | 5 v61 | 2 | 8 v61 | β |
| Basic | basic | 699 | 1 | 100 | 10 | 365 v95 | β |
| Premium | premium | 999 | 2 | 300 | 40 | 545 v95 | β / β / β / β |
| Premium+ | premium_plus | 1,499 | 3 v61 | 500 | 100 | 730 v95 | β / β / β / β |
| Ultimate | ultimate | 1,999 | 8 v95 | 1,000 | 100 | β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
| Layer | Used for | TTL |
|---|---|---|
| In-memory (per Worker isolate) | plan_config rows (planConfig.ts) | 5 min |
| Cloudflare KV | plan_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 afterwards | 7 days, stale-while-revalidate 1 day |
HTTP Cache-Control (edge, no Worker invocation at all) | /public/event-types β fully static payload NEW | 1 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 viaPromise.all; the view-count increment andpage_viewsinsert are batched together and run as a non-blocking background task viawaitUntilso they never add latency to the response.GET /analytics/overviewandGET /analytics/:weddingIdβ every aggregate query batched into a single D1 round-trip.GET /admin/statsβ six independent COUNT/GROUP BY queries run withPromise.all.PATCH /media/reorder,checkin.tsarrival/list endpoints β already batched.
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-wasmOG-image renderer initialises once per Worker isolate (module-scope promise), not per request. - Cache busting is explicit and targeted β
WEDDING_CACHE_VERSIONbump +bustPlanConfigCache()invalidate only the KV keys that changed, rather than flushing broadly. - Telemetry pruning β the daily cron (
expiry.ts) deletespage_views/whatsapp_messagesolder than 90 days andaudit_logolder 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 exactWHEREclause),idx_guests_rsvpon(wedding_id, rsvp_status),idx_page_views_wedding_timeon(wedding_id, viewed_at DESC)β every hot-path query inanalytics.ts,public.ts, andcheckin.tsis covered by an index shaped for its actualWHERE/ORDER BY.
/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
| Requirement | Version | Install |
|---|---|---|
| Node.js | β₯ 20.0.0 | nodejs.org |
| pnpm | β₯ 9.0.0 | npm install -g pnpm |
| Wrangler CLI | β₯ 4.40.0 | npm install -g wrangler |
| Cloudflare account | Free tier | cloudflare.com |
| Cloudinary account | Free tier | cloudinary.com |
| Razorpay account | β | razorpay.com |
| Resend account | Free tier | resend.com β password reset & expiry emails only |
| MSG91 account | Free tier | msg91.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 ../..
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:
| Migration | Version | Summary |
|---|---|---|
| 029_v93_corporate_event_types.sql | v93 | Adds the Corporate Events community's sub-event types (keynote, workshop, networking, awards_ceremony, team_activity) |
| 030_v93b_corporate_templates.sql | v93 | 7 corporate-themed templates (Product Launch, Conference, Annual Day, Award Ceremony, Team Offsite) |
| 032_v95_plan_config_sync.sql | v95 | Syncs 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.sql | v100 | Gift 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.sql | v122 | Itemised Gift Wish List (weddings.gift_items JSON column) |
| 039_v123_demo_events_enrichment.sql | v123 | Enriches all 15 demo invitations with v100+ features; adds 3 new demos (Corporate, Naming Ceremony, Birthday) |
| 040_v133_payment_safety.sql | v133 | Payment safety hardening |
| 041_v135_unified_expiry.sql | v135 | Sets weddings.page_expires_at = users.plan_expires_at for all active paid plans β unified expiry |
| 042_v137_max_videos_plan_config.sql | v137 | Adds max_videos column to plan_config |
| 043_v140_event_types_expand.sql | v140 | Expands events.event_type CHECK constraint to 43 valid values (28 new types) |
| 044_v140_new_templates.sql | v140 | Seeds 15 new templates to templates table |
| 039_v123_demo_events_enrichment.sql | v123 | Brings 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
| Command | Action |
|---|---|
| pnpm deploy | Full deploy: DB migrations β API β Admin β Public |
| pnpm deploy:db | DB migrations only |
| pnpm deploy:code | All three packages, no DB |
| pnpm deploy:api | API Cloudflare Worker only |
| pnpm deploy:admin | Admin SPA only (Cloudflare Pages) |
| pnpm deploy:public | Public Astro SSR only (Cloudflare Pages) |
| pnpm db:fresh | β οΈ Drop & recreate all tables (fresh install only) |
| pnpm db:migrate:remote | Run all 39 incremental migrations on production |
| pnpm db:version | Print current schema version from production DB |
| pnpm admin:reset | Reset admin password back to PBKDF2_PLACEHOLDER |
| pnpm secrets:set | Interactive secret setup helper |
| pnpm typecheck | Run 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_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
{ email, password, plan_key, coupon_code? } β Returns { token, user }. user.plan_expires_at included since v53.{ email, password } β Returns { token, user }{ user: { id, email, role, plan, plan_expires_at } } in a single query (merged since v46).{ 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).
{ 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.bustWeddingCache (uses WEDDING_CACHE_VERSION constant). Returns 404 { error: "Event not found" } if not owned by caller.wedding:slug:{WEDDING_CACHE_VERSION}Guests & RSVP
{ 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).{ 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
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.{ items: [{id, sort_order}] }. Batched into one D1 round-trip.{ items: [{id, sort_order}] }. Batched into one D1 round-trip (fixed in this performance pass to match /media/reorder).Check-in & Live Updates
{ guest_id?, guest_name, notes? }. Works for walk-ins without a pre-existing guest row too. NEW v100live_update_text + live_update_at, shown as a banner on the public invitation page β e.g. "Ceremony running 15 min late."Payments
?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)".{ plan_key, coupon_code? }. Server validates coupon server-side, creates Razorpay order, returns order_id.{ 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.payment.captured and payment.failed events. Validates webhook HMAC signature using RAZORPAY_WEBHOOK_SECRET (typed since v54).Admin (Super-Admin Only)
{ code: "SAVE20", discount_pct: 20 }. Code is 6 chars; last 2 digits encode discount %.404 { error: "Event not found" } if missing.{ 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)
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:
| Provider | Used for | Why split |
|---|---|---|
| Resend | Password reset, plan-upgrade confirmation, page-expiry warnings β system/transactional mail to the host | Reliable transactional delivery; generous enough free tier for low-volume system mail |
| MSG91 | Bulk "Email all guests" invite sends from the editor β guest-facing mail, can be hundreds of recipients per event | Added 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 fromplan_config.expiry_daysfor 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_viewsandwhatsapp_messagesrows older than 90 days andaudit_logrows older than 180 days, keeping D1's free-tier row count from growing unbounded as the platform scales - Reads
RESEND_API_KEYandAPP_URLdirectly fromenv(typed, no casts since v54)
ποΈ Database Migrations
| File | Version | Changes |
|---|---|---|
| 000_full_schema.sql | Canonical | Complete 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.sql | v31 | DB-driven plan_config table |
| 010_v32_sort_order_show_public.sql | v32 | Template sort_order + show_public flag |
| 011_v34_production_fixes.sql | v34 | Production data fixes |
| 012_v35_audit_fixes.sql | v35 | Audit log improvements |
| 014_v41_event_description.sql | v41 | Event description field |
| 015_v43_production_fixes.sql | v43 | Production index + constraint fixes |
| 016_v46_auth_query_fix.sql | v46 | Auth query performance fix (marker only) |
| 017_v48_discount_codes.sql | v48 | Coupon system tables (discount_codes) |
| 018_v50_coupon_fixes.sql | v50 | Coupon validation fixes |
| 019_v51_content_music_fields.sql | v51 | Music + content block fields (dresscode, custom_message, etc.) |
| 020_v54_type_safety.sql | v54 | Marker migration β type safety release, no schema changes |
| 021_v59_demo_events.sql | v59 | 12 curated demo invitations covering all communities |
| 022_v61_plan_tier_updates.sql | v61 | Free 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.sql | v65 | PWA icon/splash-screen refresh β marker only, no schema change |
| 024_v67_ultimate_never_expires.sql | v67 | Ultimate plan's expiry_days set to β1 (never expires) |
| 025_v68_responses_email_wa.sql | v68 | KEY 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.sql | v69 | Marketing/content-only β homepage features section rebuilt, no schema change |
| 027_v71_performance.sql | v71 | KEY Major performance pass β composite covering indexes, D1 query batching, two-tier plan_config caching |
| 028_v81_platform_gallery.sql | v81 | platform_gallery table β curated real-wedding showcase for the landing page |
| 029_v93_corporate_event_types.sql | v93 | KEY Corporate Events community's sub-event types (keynote, workshop, networking, awards_ceremony, team_activity) |
| 030_v93b_corporate_templates.sql | v93 | 7 corporate-themed templates |
| 031_v94_schema_version.sql | v94 | Schema version bump marker |
| 032_v95_plan_config_sync.sql | v95 | Synced plan_config expiry days & Ultimate's max_weddings to live values |
| 033_v100_new_features.sql | v100 | KEY Gift registry, hero video, love-story timeline, vendor credits, live updates, save-the-date toggle, QR code, guest_checkins table |
| 034_v101_css_fixes.sql | v101 | CSS fixes β marker only |
| 035_v109_version_bump.sql | v109 | Version bump marker |
| 036_v110_whatsapp_fixes.sql | v110 | WhatsApp reminder & personalised-queue endpoint additions (no schema change) |
| 037_v113_version_bump.sql | v113 | Version bump marker |
| 038_v122_gift_items.sql | v122 | Itemised Gift Wish List (weddings.gift_items JSON column) |
| 039_v123_demo_events_enrichment.sql | v123 | NEW 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.
| Theme | Shipped in | What's true today |
|---|---|---|
| WhatsApp delivery | v68, v110 | Free 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 email | v68 | MSG91 Email API for guest-facing bulk sends, alongside Resend for system/transactional mail. |
| Performance pass | v71 | Composite covering indexes, D1 batch queries, and two-tier (in-memory + KV) caching for plan limits β see Performance & Caching. |
| Platform gallery | v81 | A curated, cross-customer real-wedding showcase on the public landing page, managed from the admin panel. |
| Corporate Events | v93 | Corporate 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 blocks | v100 | Gift 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-in | v100 | A 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 Studio | pre-v100, v140 | Client-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 Writer | pre-v100 | On-device phrase-bank story generator (storyGenerator.ts). Template-based, not a call to an external LLM β zero cost, zero latency, works offline. |
| Gift Wish List | v122 | Itemised gift list (gift_items JSON column) β name, note, product link β as alternative/complement to gift_registry_url. Section renders when either is set. |
| Demo showcase | v59, v123 | 15 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 export | v126 | GET /weddings/:id/export-html and GET /weddings/:id/export-story-html β self-contained HTML download with branding watermark. Button in Admin β Settings. |
| Content protection | v125, v128 | Client-side right-click, copy, print, DevTools keyboard-shortcut prevention. public-utils.js covers all public pages; admin-protect.js covers the admin SPA. |
| Registration UX | v134 | Single-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 expiry | v135 | users.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 hardening | v136 | Global 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 Hub | v139 | ResponsesPage 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 expansion | v140 | 28 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 detection | v140 | Admin 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 templates | v140 | 3 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-link | v140 | "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
--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
| Role | Password | |
|---|---|---|
| Demo couple | demo@einvite.app | TalDemo@2026 |
| Super admin | admin@einvite.app | ADMIN_BOOTSTRAP_PASSWORD secret |
demo-* slugs.
Kerala, India Β· Powered entirely by Cloudflare's free stack Β· Β© 2026