A matrimony site where the Guru's chart-matching method ranks and explains every match, built on Cloudflare with all matching logic on the server.
We are building a matrimony site where the Guru's chart-matching method ranks and explains every match, running entirely on Cloudflare with all matching logic on the server.
The reference site (mayyam.in) supplies the general shape: register, build a profile, search with filters, view a profile, express interest. Our difference is that birth data is a required input, charts are computed by our backend, and search results are ordered by compatibility for the logged-in member.
Everything runs on Cloudflare: one front-end Worker for pages, one API Worker for data and logic, D1 for records, R2 for photos. The front end never talks to D1 or R2 directly.
| Component | Cloudflare product | Job |
|---|---|---|
| Web front end | Workers (Next.js via OpenNext, or Astro) | Server-rendered pages: home, search, profile, account |
| API | Workers (Hono router) | /api/v1/*: auth, profiles, search, interests, admin |
| Database | D1 (SQLite) | All relational data, accessed through Drizzle ORM with migrations |
| Files | R2 + signed URLs | Original photos kept private; blurred and watermarked variants generated on upload |
| Sessions and lookups | KV | Session tokens, cached dropdown lists (castes, stars, districts) |
| Background work | Queues + Match Worker | Compute chart on profile save; recompute match scores |
| Bot protection | Turnstile + WAF rate limits | Register, login, OTP and search endpoints |
| Email and SMS | External provider (e.g. Resend, MSG91) | OTP, interest notifications |
| Payments | Razorpay (webhook into API Worker) | Plan purchase and renewal |
On D1. It is a good starting point: cheap, zero-ops, and fine for tens of thousands of profiles. Its limits are a 10 GB cap per database, a single write region, and no full-text or geo search. Keep all SQL behind a repository layer so a later move to Postgres (through Hyperdrive) touches one module. Do not put queries in route handlers.
On the chart library. Swiss Ephemeris is the standard for planetary positions and compiles to WebAssembly, which Workers can run. It is dual-licensed (AGPL or a paid licence), so decide on the licence before using it in a closed-source product. If the WASM bundle proves too heavy for a Worker, the fallback is a small container service behind the Match Worker; nothing else in the design changes.
The profile is split across tables by sensitivity, so a query for a guest card cannot accidentally select contact or birth data.
Fields the team expects to add or remove later go in a JSON extras column rather than new migrations.
| Table | Holds | Sensitivity |
|---|---|---|
users | id, email, phone, password hash, role (member, staff, admin), status, created_at | Private |
sessions | token hash, user_id, expires_at, user agent | Private |
otp_codes | phone or email, code hash, purpose, attempts, expires_at | Private |
profiles | id, user_id, handle, display name, gender, dob, height, weight, marital status, mother tongue, body type, complexion, physical status, habits, about text, status, plan, extras JSON | Card fields public; dob members only |
profile_religion | religion, caste, sub-caste, gothram, kulam temple, star, raasi, dosham | Members only |
profile_professional | education, specialization, institution, sector, industry, job title, income band | Members only |
profile_location | country, state, district, city, address | City public; address paid only |
profile_family | parents' names and status, family type, values, status, siblings, house, land, assets | Paid or mutual interest |
profile_contacts | relationship, mobile, email | Paid or mutual interest |
profile_photos | R2 keys for original, blurred and watermarked variants; sort order; approval status | Variant depends on tier |
birth_data | birth date, time, place, latitude, longitude, timezone, time accuracy | Private; never sent to clients |
charts | profile_id, method version, computed placements as JSON, computed_at | Summary fields members only |
partner_preferences | age and height ranges, marital status, habits, religion, caste, star, dosham, education, income, location | Members only |
match_scores | profile_a, profile_b, method version, score, breakdown JSON, computed_at | Own matches only |
interactions | from, to, type (view, shortlist, interest, contact request), state, created_at | Own rows only |
saved_searches | user_id, name, filter JSON | Own rows only |
plans, payments | plan definitions; Razorpay order and payment ids, status, period | Private |
lookups | type (caste, star, education, district...), id, label per language, parent_id | Public |
audit_log | actor, action, target, timestamp | Staff only |
Indexes that search depends on: profiles(status, gender, dob), profile_religion(caste, star), profile_location(state, district), match_scores(profile_a, score).
Profile status values: REGISTERED (account only), PENDING (submitted, awaiting approval), ACTIVE, HOLD, HIDDEN, CLOSED. Only ACTIVE profiles ever appear in search or on public URLs.
Registration is a short account step followed by a guided profile wizard, and a profile goes live only after staff approval.
Phone OTP is the primary identity check because most members will register from a phone, often a parent on behalf of the bride or groom.
HttpOnly, Secure, SameSite=Lax session cookie holding a random token. Only a hash of the token is stored. Sessions last 30 days and slide on use.{user_id, role, plan} in middleware. Handlers receive that context and never read the cookie themselves.| Endpoint | Purpose |
|---|---|
POST /api/v1/auth/register | Create account, send OTP |
POST /api/v1/auth/otp/verify | Verify phone or email |
POST /api/v1/auth/login | Password or OTP login, sets session cookie |
POST /api/v1/auth/logout | Delete session |
POST /api/v1/auth/password/reset | OTP-based reset |
GET /api/v1/me | Current user, role, plan, profile completion |
One serializer in the API Worker decides which fields a viewer receives, and every endpoint that returns a profile goes through it.
The front end shows a "register to view" or "upgrade to view" prompt wherever a section arrives empty.
| Section | Guest | Free member | Paid member | Mutual interest accepted |
|---|---|---|---|---|
| Card: first name, age, height, city, education, occupation | Yes | Yes | Yes | Yes |
| Photos | Blurred, first only | Watermarked | Watermarked, all | All |
| Religion, caste, star, raasi, dosham | No | Yes | Yes | Yes |
| Compatibility score with me | No | Score only | Score and breakdown | Score and breakdown |
| Professional detail, income band | No | Yes | Yes | Yes |
| Family details | No | No | Yes | Yes |
| Chart (raasi and navamsam view) | No | No | Yes | Yes |
| Full name, mobile, email, address | No | No | Limited by plan quota | Yes |
| Exact date, time and place of birth | No | No | No | Only if the owner opts in |
The exact tiers are a business decision for the Guru; the table is a starting proposal. Whatever is chosen, the rule is that a field a viewer may not see is absent from the JSON, not hidden by the page.
Profile page. GET /p/{handle} is server-rendered by the Web Worker, which calls GET /api/v1/profiles/{handle} with the viewer's session. Sections mirror the data model: about, basic details, religion and horoscope, professional, location, family, lifestyle, partner preferences, contact. Non-active profiles return 404 to everyone except the owner and staff.
| Action | Endpoint | Notes |
|---|---|---|
| Record a view | automatic on profile fetch | One row per viewer per day |
| Shortlist | POST /api/v1/profiles/{handle}/shortlist | Private to the member |
| Send interest | POST /api/v1/profiles/{handle}/interest | Recipient can accept or decline; daily cap by plan |
| Respond to interest | POST /api/v1/interests/{id}/respond | Accepting unlocks the mutual-interest column |
| Request contact | POST /api/v1/profiles/{handle}/contact | Deducts from plan quota; logged |
| Report or block | POST /api/v1/profiles/{handle}/report | Goes to the staff queue |
Editing. Members edit their own profile section by section through PATCH /api/v1/me/profile/{section}. Changes to name, about text and photos go back through approval; other fields apply immediately. Any change to birth data queues a chart recompute.
Search is one endpoint where every filter is optional, and the page URL always mirrors the filters in use.
A search with no values and a search with ten values run the same code path; the only difference is how many conditions are added to the query.
GET /api/v1/search?gender=F&age_min=24&age_max=30&caste=12,15&star=4&state=TN&sort=match&page=2
The server applies defaults rather than returning everything.
| Viewer | Defaults applied |
|---|---|
| Guest | Gender is required (the form asks for it); age 21 to 35; with photo; sorted by newest; first 3 pages only |
| Member, no saved preferences | Opposite gender; age band around the member's own age; sorted by compatibility |
| Member with partner preferences | Preferences pre-fill the filters and the URL, so the member can see and relax them |
Each parameter that is present adds one condition. Absent or empty parameters add nothing.
| Parameter | Type | Condition |
|---|---|---|
gender | M or F | equals |
age_min, age_max | integer | date of birth range |
height_min, height_max | cm | range |
marital_status, mother_tongue, caste, sub_caste, star, dosham, education, occupation, employed_in, state, district, country | comma-separated lookup ids | IN (...) |
income_min, income_max | income band id | range |
physical_status, eating, own_house, family_status | enum | equals |
with_photo, with_horoscope | boolean | exists |
min_score | 0 to 100 | compatibility with the logged-in member |
exclude | viewed, shortlisted, contacted | not in the member's interactions |
q | text | profile id or handle lookup |
sort | match, newest, age | order; match only when logged in |
page, page_size | integer | page_size capped at 20 |
Fixed conditions on every search, which no parameter can override: status is ACTIVE, the viewer's own profile and blocked profiles are excluded, and results pass through the visibility serializer.
{
"total": 1240,
"page": 2,
"page_size": 20,
"applied": { "gender": "F", "age_min": 24, "age_max": 30, "caste": [12, 15] },
"results": [ { "handle": "...", "first_name": "...", "age": 27, "height_cm": 160,
"city": "...", "education": "...", "occupation": "...",
"photo": "https://.../blurred.webp", "score": 82 } ]
}
applied echoes the filters the server actually used, including defaults, so the page can show them as removable chips. score is present only for logged-in members.
history.replaceState and re-fetch after a 300 ms pause, so the back button and shared links work.match joins match_scores for the logged-in member. Profiles without a score yet sort last and are queued for scoring.GET /api/v1/lookups/{type}, cached in KV and at the edge. District depends on state, sub-caste on caste.POST /api/v1/me/searches stores the filter object under a name.The Guru's method lives in one server-side module with a fixed interface, so the rest of the site can be built before the rules are final.
Nothing about the rules, weights or intermediate values is shipped to the browser; clients receive a score and an explanation.
charts, pick candidates gender, age, preferencesmatch_scores| Function | Input | Output |
|---|---|---|
computeChart | birth date, time, place coordinates, timezone, time accuracy | lagna, planet placements by sign and house, nakshatra and pada, navamsa, dasha periods, doshams, plus the method_version |
scoreMatch | two charts, optional profile facts (age gap, preferences) | score 0 to 100, verdict (recommended, acceptable, not recommended), breakdown as a list of {rule, result, points, note} |
explain | breakdown, language | member-readable text in English or Tamil |
method_version that produced it. When the rules change, bump the version and let the queue recompute in the background; old scores stay visible until replaced.| Endpoint | Returns |
|---|---|
GET /api/v1/me/matches?page=1 | The member's best matches by score |
GET /api/v1/profiles/{handle}/match | Score and, by tier, the breakdown against the logged-in member |
GET /api/v1/profiles/{handle}/chart | Rendered chart data for display (paid tier) |
POST /api/v1/admin/method/recompute | Queue a recompute for a version (admin only) |
Staff tools are a separate app on their own hostname (for example admin.), protected by Cloudflare Access, so no staff screens or staff-only fields are ever bundled into the member site.
Payments. Razorpay Checkout on the plan page. The API Worker creates the order, and a signed webhook (POST /api/v1/webhooks/razorpay) is the only thing that activates a plan. Plans define duration, contact-view quota and daily interest cap. Offline payments at the Guru's office are entered by staff and flow through the same plan activation code.
A matrimony site holds birth details, caste, income, family and phone numbers, so the API must be safe even if someone calls it directly without the website.
These are the requirements to review each endpoint against.
Six phases take the site from empty repository to public launch in about 18 weeks for a team of two or three developers.
The durations are estimates to be revised once the team and the Guru's rule set are known.
| Phase | Weeks | Delivers | Done when |
|---|---|---|---|
| 0. Foundations | 1 to 2 | Repos, Workers projects, D1 migrations, dev/staging/prod environments, CI deploys, design system, lookup data | A hello-world page and API deploy to staging on every merge |
| 1. Accounts and profiles | 3 to 5 | Register, OTP, login, profile wizard, photo upload with variants, profile page, visibility serializer, approval queue | A tester can register, be approved and view another profile with correct redaction at each tier |
| 2. Search | 6 to 8 | Search endpoint, filter UI with URL sync, defaults, pagination, saved searches, lookups API | Search with and without values matches the spec; guest limits enforced |
| 3. Charts and matching | 7 to 11 | Birth data capture with geocoding, chart computation, rule engine v1, background scoring, match sort, explanation text | Engine reproduces the Guru's verdicts on the hand-judged test pairs |
| 4. Interaction and payments | 10 to 13 | Shortlist, interests, contact requests with quotas, notifications, Razorpay plans, staff support tools | A paid member can complete interest, acceptance and contact reveal end to end |
| 5. Hardening and launch | 14 to 18 | Tamil language, security review, load test, DPDP review, backups, analytics, seeded launch with the Guru's existing clients | External security review passed; first 100 consented real profiles live |
| 6. After launch | ongoing | Mobile app or PWA, community sub-sites, method console improvements, move to Postgres if D1 limits are near | Driven by usage |
Phases 2 and 3 overlap on purpose: search does not depend on the engine, and the engine work can start as soon as the Guru's rules are written down.
These need answers before the affected phase starts; the first three block the matching engine design.