Add Vedic matchmaking to a Next.js app in 30 minutes

One server action, one form, and the API key never leaves the server. You get the Ashtakoot score out of 36, the eight kootas, and Rajju and Vedha reported as vetoes rather than buried in the points.

The finished app is at github.com/asterwise/nextjs-vedic-matchmaking. It is about 120 lines of TypeScript on top of a fresh create-next-app. This post walks through those lines and the two decisions in them that matter.

What you need

  • Node 20 or later and a free Asterwise key from the dashboard. The Sandbox tier is 500 calls a month, no card, and each match is one call.
  • The asterwise package from npm. It is generated from the API's OpenAPI document, so the request and response types match what the server actually sends.

1. Create the app and install the SDK

npx create-next-app@latest nextjs-vedic-matchmaking --ts --app --src-dir --eslint --no-tailwind
cd nextjs-vedic-matchmaking
npm install asterwise
cp .env.example .env.local   # ASTERWISE_API_KEY=aw_...

Put the key in .env.local as ASTERWISE_API_KEY, not as a NEXT_PUBLIC_ variable. Anything prefixed NEXT_PUBLIC_ is bundled into the browser, and a key in the browser is a key anyone can spend.

2. One client, server-side only

src/lib/asterwise.ts builds a single client for the process. The throw at import time is deliberate: a missing key should fail the first request loudly, not return empty results.

import { createClient, createConfig } from "asterwise/client";

const apiKey = process.env.ASTERWISE_API_KEY;
if (!apiKey) {
  throw new Error("ASTERWISE_API_KEY is not set. See .env.example.");
}

export const asterwise = createClient(
  createConfig({
    baseUrl: "https://api.asterwise.com",
    headers: { Authorization: `Bearer ${apiKey}` },
  })
);

3. The server action

A server action runs on the server and can be passed straight to a form. It reads both people from the form, calls matchmaking(), and returns either the typed response or a message the page can show.

"use server";

import { matchmaking } from "asterwise";
import type { MatchmakingResponse } from "asterwise";
import { asterwise } from "@/lib/asterwise";

export type MatchResult =
  | { ok: true; data: MatchmakingResponse }
  | { ok: false; error: string };

export async function runMatch(_prev: MatchResult | null, form: FormData): Promise<MatchResult> {
  const p1 = readPerson(form, "p1");   // name, date, time, location
  const p2 = readPerson(form, "p2");

  const res = await matchmaking({
    client: asterwise,
    body: {
      // person1 is the groom and person2 the bride in the classical Ashtakoot method.
      person1: { name: p1.name || undefined, date: p1.date, time: p1.time || null, location: p1.location },
      person2: { name: p2.name || undefined, date: p2.date, time: p2.time || null, location: p2.location },
    },
  });

  if (res.error || !res.data?.data) {
    return { ok: false, error: res.error?.message ?? `Request failed (${res.response.status}).` };
  }
  return { ok: true, data: res.data.data };
}

Two things to notice. Birthplace is a string. The API geocodes it and resolves the time zone, so the form needs no latitude, longitude or zone fields. Time is optional. If it is blank the API casts a sunrise chart for that person and sets birth_time_provided: false; the Moon-based kootas are still exact, and only the ascendant-based checks become approximate. Show that flag to the user rather than hiding it.

4. The form and the result

useActionState wires the action to the form and gives you a pending flag for the button. The result render is where the design decision lives:

const vetoes = (d.classical_vetoes ?? {}) as Vetoes;

{vetoes.has_veto ? (
  <p className={styles.veto}>
    Classical veto present. {vetoes.rajju?.present && `Rajju (${vetoes.rajju.rajju_type}). `}
    {vetoes.vedha?.present && "Vedha. "} {vetoes.veto_note}
  </p>
) : (
  <p className={styles.clear}>No Rajju or Vedha veto.</p>
)}

The veto line sits above the score on purpose. In the classical method, Rajju or Vedha between the two Moon nakshatras stops the match regardless of how many of the 36 points it collects. Many implementations fold those into a penalty, which produces a reassuring 28/36 for a pairing the texts would reject. Asterwise returns them as separate fields so you can show them separately. The reasoning is in Rajju and Vedha as hard vetoes.

Under the veto line, the page shows the score, the eight kootas from breakdown, and the compatibility_narrative: an overall paragraph, strengths, concerns and a recommendation, all as plain strings you can render or feed to a model.

5. Run it

npm run dev      # open http://localhost:3000
npm run smoke    # one call from the command line; prints the score and the koota table

For the sample pair the form is prefilled with, the live API returned 16.5 out of 36, level Average, no Rajju or Vedha veto, with Bhakoot at 7 and Gana and Nadi at 0. The smoke script also prints whether the response carried the X-Asterwise-Signature header. Every Asterwise response is HMAC-signed, so a stored result can later be shown to have come from the API unaltered.

Where to take it

  • Add matchmakingDashakoot for the ten-koota South Indian method, or matchmakingPorutham and matchmakingThirumanaPorutham for Kerala and Tamil conventions. Same two-person request shape.
  • Cache results by the pair of inputs. The API is deterministic for a given birth data, so one call per pair is enough.
  • Show the ayanamsa. The default is Lahiri; pass ayanamsa: "raman" or "kp" on each person if your users expect those.

Every Asterwise position is checked against NASA JPL Horizons, and the matchmaking rules follow the classical texts rather than a points-only shortcut. The free tier is 500 calls a month with no card.