Skip to main content

Changelog

What’s new

Visible record of what shipped, in chronological order. Newest first. Production lives at vettika.com.

  1. LatestFeatureFixInfra

    Trusted devices inventory + ownership audit + Google secret rotation

    Post-2FA follow-ups. Users can now see and revoke each "Remember this device for 30 days" trust cookie on /account, with human labels like "Chrome on macOS · 102.x.y.z" instead of a random suffix. A candidate-side RSC ownership audit closed two read holes. The Google OAuth client secret was rotated end-to-end after the 2026-05-23 chat leak.

    • Trusted devices list + per-row revoke + revoke-all on /account
    • Human labels from User-Agent: "Chrome on macOS", "Safari on iPhone", etc.
    • IP shown alongside each trusted browser (best-effort header parse)
    • Better Auth hooks.after writes sidecar on /two-factor verify
    • TrustedDeviceMeta sidecar via direct SQL on Neon (skip Prisma db push)
    • Card hides itself entirely when 2FA off — no empty-list noise
    • Revoke deletes Verification row -> trust cookie silently expires next sign-in
    • "Remember this device for 30 days" toggle on /two-factor sign-in
    • GOOGLE_CLIENT_SECRET rotated in Google Console + Vercel (3 scopes)
    • F1 probe verified: /api/auth/sign-in/social returns valid OAuth URL
    • CLAUDE.md secret-leak flag closed; GH #17 closed
    • /sessions/[id] now gates on recruiterId — closed a read hole
    • Candidate-page URL consistency: campaignId mismatch -> 404
    • previewCandidateFeedback gated via assertCandidateOwnership
    • listAutomationActivity + retryAutomationActivity scoped by rule.recruiterId
    • eslint.config.mjs ignores *.test.{ts,tsx} (fixes import/named false positives)
    • 2FA backup-code regeneration button on TwoFactorCard
    • Agent: resolve_temperature() extracted to pure helper + 7 unit tests
    • E3 LLM temperature override loop verified end-to-end
    • 2FA + trust-device smoke test parked at docs/qa/2fa-smoke.md
    • GH #40 (RSS), #41 (share buttons), #42 (comments) filed for jobs board

    Commits: f26817d, b8b98dd, 50dd52f, 16edd16, 6d7f17f, 2384f6b, 1e8d8aa, 64ee372, 5d7a6cd, dc7035c

  2. FeatureFixDesign

    Candidate flow: kill slots, gate on join code, provision Interview row

    Three fixes on the candidate path. Time-slot picking was throwing "Selected slot is no longer available" for every slot the candidate tried — replaced with a simpler "open vs limited" cap. The 6-digit join code emailed at apply time is now a real server-checked gate on /i/[token]/check before LiveKit will mint a token. And /i/[token]/live was silently falling back to a mock LiveKit URL when no Interview row existed, which immediately disconnected the candidate to a generic /error — now provisions the row server-side from the campaign config and forwards LiveKit DisconnectReason through ?reason= so the error page can render specifics.

    • Apply link: removed time slots, calendar picker, fallback self-schedule; now only enabled + maxApplications
    • ApplyLink.slots JSON column kept as no-op for forward compat (no migration needed)
    • POST /api/public/invite/[token]/verify-code: matches Candidate.joinCode, sets HTTP-only cookie ic_v_<token> (2h, sameSite=lax, secure)
    • CandidateInvitePublic.joinCode dropped from public payload → requiresJoinCode boolean (stops leaking the code)
    • SystemCheck: numeric join-code Card after mic/speaker pass, inputMode=numeric + autoComplete=one-time-code for iOS autofill
    • /i/[token]/live server guard: missing cookie → redirect /check
    • ensureInterviewForCandidate snapshots role/intro/closing/questions/rubric/duration from Campaign on first /live load
    • Killed startInterview() mock fallback in <LiveInterview> (was returning wss://example.livekit.cloud + MOCK_LIVEKIT_JWT)
    • Connecting preview rebranded: in-call orb + first name + "Setting up your interview" headline
    • friendlyDisconnectReason() maps LiveKit JOIN_FAILURE / SERVER_SHUTDOWN / SIGNAL_CLOSE / STATE_MISMATCH / DUPLICATE_IDENTITY to actionable copy via ?reason=
    • /error page: AlertTriangle icon plate, eyebrow "Connection interrupted", support@vettika.com footer with truncated invite token
    • Brute-force guard on verify-code: 10 attempts/min per (IP + token) via existing checkRateLimit

    Commits: de86d9b, a7b386d, bd01dbb

  3. FeatureInfraFix

    Account security hardening — TOTP 2FA, Turnstile, HIBP, re-auth

    Closes the user-account security track. Sign-in is gated by Cloudflare Turnstile, passwords are checked against the HIBP breach corpus, sensitive actions require a fresh session, new devices trigger an email alert, and TOTP two-factor auth ships end-to-end (enable on /account, verify on sign-in, backup codes, disable).

    • TOTP 2FA enable flow on /account: password → QR + backup codes → TOTP verify
    • /two-factor sign-in step with TOTP + backup-code fallback, preserves ?next=
    • authClient twoFactorClient plugin: onTwoFactorRedirect → /two-factor
    • Login page reads twoFactorRedirect from signIn.email response and routes through 2FA
    • Better Auth twoFactor plugin server-side, issuer "Vettika", skipVerificationOnEnable false
    • Cloudflare Turnstile widget on /login, /signup, /forgot-password (env-gated)
    • HIBP k-anonymity password breach check on signup + password change
    • New-device email alert: session.create.after hook + KnownDevice fingerprint table
    • Fresh-session gate on delete-account, unlink-Google with ReauthDialog replay
    • Profile card: Resend verification button when emailVerified=false
    • Sessions card: first-seen / last-seen device history from KnownDevice join
    • Account self-service: export progress toast with file size
    • Server Actions proxy (lib/actions.ts) keeps Prisma out of browser bundles
    • Single email delivery path lib/email/send.ts; killed typeof-window send gate
    • aiRouteUrl() helper stops /api/ai/* self-loop on server context
    • encryptToken returns Uint8Array<ArrayBuffer> for Prisma 6 Bytes typing
    • Neon keepalive: /api/health route + cron-job.org every 5 min
    • Polar.sh stays default merchant-of-record; Stripe webhook signature verification live

    Commits: dc7035c, 65a659c, 0fa6596, f6f1e0c, d4ec400, e861b17, 453e53c, eebc3d7

  4. FeatureInfra

    Auth + Settings + Billing — Session 1 recap

    Closing the auth + billing + settings track. Real user identities, settings that actually persist, billing wired end-to-end with Polar as the default merchant-of-record and Stripe waiting for an Atlas-shaped US entity.

    • Route groups: marketing, app, candidate, auth
    • Prisma Session → Interview rename, @@map preserves data
    • Better Auth schema generated, four tables on Neon
    • /api/auth handler + Edge middleware gate
    • Email + password + verify + reset + welcome
    • Sign-up gate: prompt for new Google identity
    • Last-used "Continue as you" login card
    • 68 unit tests + 19-check auth smoke script
    • XSS-safe email template escaping
    • Group layouts: MarketingHeader + MegaFooter
    • Auto-scaling wordmark in the marketing footer
    • Account page: profile, password, sessions, danger
    • Link / unlink Google on /account
    • WorkspaceCard counts on /account
    • Workspace prefs persist on the User row
    • Integrations flipped honest: all "coming soon"
    • Data-privacy retention persist + JSON export
    • Notification prefs persist; candidate_applied email lives
    • Billing page: real usage, free tier honest
    • Stripe checkout, portal, signed webhook handler
    • Script: auto-create the four Stripe prices
    • Multi-provider checkout abstraction (one file per provider)
    • Polar.sh wired as default merchant-of-record
    • Stripe Atlas activation path documented in CLAUDE.md
    • Double-rendered settings chrome fixed

    Commits: f43cfed, 57125cd, e145285, 69b9d1f, 0bc9736, 75de80a, 94036bc, 22fc784, dd7cfa6, c7f3929, 21e62e5, a4bee8b, ff472dd, a7e6492, 67a6e54, 25a4725, 2697a0b

  5. Docs

    Session recap — what shipped end-to-end

    Single ledger of every capability landed across the multi-day persistence + product push. Each bullet is one feature — tight on purpose so /changelog reads as a complete shipped-list at a glance.

    • Jobs board with JobPosting JSON-LD
    • Recruiter inline job-meta editor
    • Automation execution engine + approval inbox
    • Candidate feedback flow with LLM tips
    • Live recruiter observer mode
    • Real LiveKit room on candidate side
    • Shortlist preset tabs + bulk actions
    • Marketing-vs-app nav split
    • Session-aware nav with dashboard CTA
    • Inline edit + delete on notes
    • Regenerate join code + resend invite
    • Persist Campaign, Candidate, ApplyLink to Postgres
    • Persist AI profile fields and default flag
    • Persist email dispatches + feedback tokens
    • Persist interview reports with agent bridge
    • Persist pending dispatches with audit trail
    • Persist integrations + live analytics counts
    • Persist automation templates + activity log
    • Resend bounce webhook + providerId capture
    • Public reschedule endpoint from candidate error page
    • Auth-context helper threads recruiterId everywhere
    • Real companyName resolved across emails + pages
    • Rate-limit on submitApplication
    • Atomic getPublicJob lazy-create
    • DB-backed dispatcher idempotency surviving restarts
    • Cursor pagination on candidates, emails, activity
    • Friendly Google OAuth no-account card
    • Observer null-recruiter bypass closed
    • Fixed /apply/invalid 404 status

    Commits: 0a4e903, 4b8876a, 61c6be8, d69e098, ff533d0, 7085a20, c537241, 6aac04d, 1233d1d, 9c1a1cf, 8a24ad9, 9c8a5ea, 467e1f6, 0b5c33b, 533092b, d10da61, 2c82edf, be15701, 3d62798, 9f2454c, 4f22062, 50057fe

  6. FixDesign

    Sign-out state isolation + empty-state polish

    Closes the last persisted-state leak between recruiters on shared devices and tightens two bare-text empty states to match the rest of the dashboard chrome.

    • New lib/auth/clear-local-state.ts wired into the avatar-menu sign-out handler — resets useCampaignDraft + useUIPrefs + useAITokens and removes the onboarding-draft-v1 localStorage key before the Better Auth round-trip. Before this commit, signing out and signing back in as a different recruiter on the same device surfaced the previous recruiter's in-flight draft campaign, recent search history, recently-viewed candidate ids, mock AI token balance, and onboarding wizard progress. useConsent is left alone — keyed by candidate token, not recruiter, so safe to leak.
    • CampaignCandidatesTable empty state now distinguishes "no candidates ever" from "filter / search hides them all" and quotes the search term back ("No candidates match \"foo\" in this view.") so the CTA points at the right action.
    • Dashboard "Recent results" tile swapped bare paragraph for a dashed-card empty state with icon + headline + sub-copy, matching the campaigns-empty and live-now tiles.

    Commits: fad9eae

  7. FixDesign

    Drain final mock placeholders from user-facing surfaces

    Candidate-facing layouts, the 404-on-bogus-apply-code page, the social OG preview, and the automation dispatcher fallback all stopped pretending to be "Acme Inc." and stopped reaching for seed data.

    • Candidate apply layout + interview layout headers swapped hardcoded "Acme Inc." for the product brand ("Vettika"). Per-recruiter company name still surfaces inside the page body via getApplyPublic.companyName + getInvite.companyName, both already resolved through resolveCampaignRecruiter → User.companyName.
    • /apply/<bogus> 404 page dropped the mailto:recruiting@example.com button. The recruiting team behind a given apply link is per-campaign and unknown when the link is bogus, so the page now directs candidates to "the recruiter who shared the link" instead and keeps only the Go-home button.
    • OpenGraph image alt swapped from "About Acme" to "Vettika — voice-AI screening interviews" so Slack / X / LinkedIn previews carry the product brand.
    • lib/automation/dispatcher.ts dropped its MOCK_INTERVIEWS[candidateId] fallback. Real recruiters could template-render mock transcript / score data into approval-queue previews if a candidate hit the dispatcher before the report row landed. Real interview only now; the existing if (report) guard below skips the feedback branch when interview is null.

    Commits: dd153e1

  8. FixInfra

    Lock recruiter ownership on listCampaigns / getCampaign / getCampaignReport

    Closes a tenant-isolation hole: anyone with a valid Better Auth session could previously read any campaign id, edit it, or open its report, and listCampaigns returned every campaign in the system instead of just the recruiter's own.

    • All three lib/api.ts read paths now resolve the active recruiter via getCurrentRecruiter() (lib/auth-context.ts) and gate on campaign.recruiterId === recruiter.id. Same denial rule as the observer-token route from 3929520: anonymous callers, legacy null-owner rows, and recruiter-mismatch rows all return null / [].
    • listCampaigns — where: { recruiterId: recruiter.id }; anonymous returns [].
    • getCampaign — returns null when not owned; callers already notFound() on null so the page surfaces as 404.
    • getCampaignReport — signature widened to Promise<CampaignReport | null>; report page now also notFound()s when report is null.
    • Middleware already redirects signed-out users from /campaigns/* to /login, so the anonymous-denial branch is defense-in-depth (closes any future non-route caller that doesn't go through middleware).
    • Follow-up audit needed for the same hole class on listCandidates, getCandidate, getInterview, and candidate-level reads — flagged in PROTOTYPE_STATUS.md backlog.

    Commits: 40c7998

  9. FixInfraFeature

    Hardening pass — rate-limits, race-free dispatch, pagination, friendlier no-account UX

    Closes the last set of ship-blockers before real users hit the app: input validation, atomic upserts, persistent dispatcher idempotency, cursor pagination on heavy lists, and a clear "no account yet" card on /login.

    • submitApplication rate-limited 5 attempts per 60s per (apply code × email) — friendly error instead of letting one bad actor flood a campaign. Bucket is in-memory today; TODO(prod) flag swaps for Vercel KV under horizontal scale.
    • updateApplyLink validates maxApplications > 0 and expiresAt > now; clearing (null) still allowed so recruiters can lift caps.
    • getPublicJob lazy-create wrapped in db.$transaction with P2002 retry — two concurrent first visitors to a freshly published role can no longer race-create duplicate apply links.
    • AutomationActivity gained idempotencyKey String? @unique. Dispatcher swapped the in-memory firedKeys Set for a DB-backed hasAlreadyFired() — dedupe now survives restarts and works across horizontally-scaled instances. persistActivity swallows P2002 so concurrent fires lose silently.
    • listCandidates / listEmailDispatches / listAutomationActivity now cursor-paginated (default 50, returns { items, nextCursor }). EmailDispatchesPanel + ActivityTable + CandidateTable use useInfiniteQuery with "Load more" / "All loaded" controls. TanStack Table row selection survives across pages via getRowId so bulk actions stay correct when the list grows.
    • New listAllCandidatesForCampaign helper for SSR paths that genuinely need the full set (dashboard analytics, candidate-review sibling walker for ↑/↓ navigation).
    • /login renders a friendly card when Google OAuth resolves to an email with no account: "No account yet for <email>." with a primary "Sign up with this Google account" (re-triggers signIn.social with requestSignUp: true) + outline "Try a different account" (clears localStorage hint, drops the error param). Stacks vertically below 480px. Same email retried inside 5min gets a terser one-line nudge via sessionStorage instead of the full card.
    • Observer null-recruiterId bypass closed — /api/token observer branch now requires a non-null recruiterId match instead of falling through on legacy seeds; localhost:3001 + :3002 whitelisted as trusted dev origins for Better Auth.

    Commits: 4f22062, 3929520, 642cb55

  10. FeatureFix

    Real recruiter ownership + company brand resolved everywhere

    Drops the recruiter@acme.example + "You" placeholders and the hardcoded "Acme Inc." company name. Every audit field + candidate-facing surface now reflects the actual signed-in recruiter and their company.

    • New lib/auth-context.ts: server-only getCurrentRecruiter() reads the Better Auth session via auth.api.getSession({ headers: await headers() }) and returns { id, name, email, companyName } from the user row.
    • createCampaign / createAIProfile / createAutomationRule now write recruiterId from the session so new rows are scoped to the creator.
    • addNote credits the real recruiter (authorName = session.user.name) and stores recruiterId on the note for joins.
    • approvePendingDispatch + rejectPendingDispatch set decidedBy to the signing recruiter's email — audit trail no longer attributed to a demo address.
    • resolveCompanyName(recruiterId) helper threads through every candidate-facing surface: sendEmail templates, getApplyPublic (apply landing), getInvite (interview join page), getPublicJob (public jobs board), getFeedbackPublic (candidate /feedback/[token] page). Falls back to "Vettika" when recruiter is null or companyName is empty, so legacy seeds keep working.

    Commits: 9f2454c

  11. InfraFeatureFix

    Agent → interview_report bridge + Resend bounce tracking + final mock drain

    Three small follow-ups that close the persistence loop end-to-end. Agent posts now flow into the recruiter-facing report table, Resend bounce/complaint events update dispatch status, and the last in-memory log buffers (AutomationTemplate + AutomationActivity) moved to Postgres.

    • Parallel sync-v1 work added Interview.candidateId/campaignId/recruiterId FKs. /api/sessions/[id]/result now dual-writes the agent-posted transcript + report into interview_report when candidateId is set, so the recruiter review summary tab + candidate /feedback/[token] page read live data without manual override.
    • sendEmail captures the Resend message id on the EmailDispatch row. New POST /api/webhooks/resend verifies Svix signatures against RESEND_WEBHOOK_SECRET and maps delivered / bounced / complained / delivery_delayed events to email_dispatch.status. Unknown events are acked so Resend stops retrying.
    • AutomationTemplate + AutomationActivity moved off MOCK arrays to dedicated Postgres tables. dispatcher.persistActivity writes every fire; rejectPendingDispatch writes straight into the activity log instead of an in-memory array. Stale deepClone helper deleted now that every DB read returns a fresh object.
    • Last MOCK_AI_PROFILES / MOCK_AUTOMATIONS / MOCK_AUTOMATION_ACTIVITY references dropped from lib/api.ts — api.ts mock-import surface is now zero.
    • AIProfile default lookup in createCampaign resolves through Prisma (caller's profile id → workspace default → first by name) instead of MOCK_AI_PROFILES.
    • Candidate /i/[token]/error page Reschedule button now hits POST /api/public/invite/:token/reschedule for real — clears scheduledFor, drops status back to invited, and queues a manual notification email to the recruiter so they see the request without polling.

    Commits: 1c6b59d, be15701, 2c82edf, 51030d6, 3d62798

  12. Feature

    AI Campaign Builder + campaign edit / report / duplicate routes

    Full-page chat-driven alternative to the 4-step wizard, plus the missing lifecycle surfaces — edit any campaign inline, view a pipeline report, duplicate to a draft, all wired end-to-end.

    • /campaigns/new/ai — split-pane (chat left / editable preview right) on desktop, tabbed on mobile. Chat accepts text + .txt/.md upload + Web Speech voice dictation. One POST /api/ai/generate-campaign call (Gemini generateObject) drafts the entire campaign — basics, public-board meta, screening questions, scoring rubric — with a canned fallback when GOOGLE_GENERATIVE_AI_API_KEY is missing.
    • Preview pane is fully inline-editable: job basics, job-board meta (department / employment type / seniority / location / comp / requirements), questions w/ follow-up depth, rubric criteria + weights, AI interviewer (profile picker + collapsible overrides incl. temperature slider 0-2), and a candidates card with shared CSV + XLSX uploader (lib/parse-candidates.ts, also used by the manual wizard) + manual-add row.
    • Mock token meter (lib/store/ai-tokens.ts) burns 1200 per full generate, 600 per per-card regenerate; manual edits are free. Save-draft re-saves patch the same id so repeated clicks don't mint duplicates.
    • /campaigns/[id]/edit mounts the same shell pre-hydrated from getCampaign; CandidatesCard is hidden in edit mode (link to detail page Add dialog instead) so accidental re-invites can't happen.
    • /campaigns/[id]/report — new getCampaignReport(id) aggregates from candidate + interviewReport.report JSON into KPI cards (candidates / completed / avg score / avg duration), pipeline-by-status bars, score distribution histogram, recommendation breakdown, last-7-day funnel.
    • duplicateCampaign(id) deep-clones source row's job / AI / rubric / meta, mints a fresh slug, resets to draft status with zero candidates, "Copy of …" title.
    • campaign-actions-menu-v2 now wires Edit, View report, Duplicate, Pause/Resume, Archive — all to real mutations with TanStack invalidation. Orphan V1 menu file deleted.

    Commits: 1b6a79e

  13. Fix

    /apply/[code] returns proper 404 on unknown codes

    Production was serving 200 with the not-found body for unknown apply codes because Vercel's CDN cached the body separately from the status. Adding force-dynamic + revalidate=0 fixes the response header.

    • Prior commit f78d214 had removed `export const dynamic = 'force-dynamic'` thinking it was redundant; without it the page rendered the not-found UI with a 200 status header from CDN.
    • Re-added `dynamic = 'force-dynamic'` + `revalidate = 0` to app/(candidate)/apply/[code]/page.tsx. generateMetadata's notFound() still drives the actual 404 header before any body streams.

    Commits: 9de5a20

  14. InfraFix

    Auth go-live + billing webhook discovery — two ops blockers cleared, one surfaced

    Cookie-domain mismatch on the custom domain is fixed; Stripe CLI is wired for local webhook smoke; a missing Stripe webhook secret in Vercel is now tracked so billing can be verified end-to-end before the first paid sign-up.

    • `BETTER_AUTH_URL` set to `https://airecruiter.nexprove.com` in Vercel Production (was an empty string — Better Auth was self-detecting the request origin so cookies bound to whichever URL landed first). Takes effect on the next push.
    • `/api/auth/ok` confirmed 200 on both the custom domain and the meetmindv3.vercel.app alias.
    • Stripe CLI v1.42.1 installed locally for `stripe listen --forward-to` + `stripe trigger` round-trips against the dev server.
    • Billing webhook smoke surfaced: `STRIPE_WEBHOOK_SECRET` is missing in Vercel Production scope AND in `.env.local`; prod `/api/webhooks/stripe` returns `400 webhook misconfigured`. Provision via Stripe Dashboard → Developers → Webhooks → `https://airecruiter.nexprove.com/api/webhooks/stripe` and paste the `whsec_*` into Vercel + local `.env.local`.
    • Final auth-go-live blocker still in your hands: F1 — paste the 3 Google OAuth callback URIs (prod, alias, localhost) into the Google Cloud Console OAuth client.
  15. InfraFeature

    Data-layer go-real — 10 entities migrated off in-memory mocks onto Neon Postgres

    Every entity the recruiter app touches now persists to Postgres. Hundreds of cold-start data losses, gone. The prototype label is retiring.

    • Workspace + recruiter ownership on Campaign / Candidate / AIProfile / Integration / AutomationRule via `recruiterId` FK; rows survive every cold start.
    • Candidate.joinCode now a persisted 6-digit column with regenerate + resend endpoints; MOCK_INVITE deleted.
    • AIProfile gained avatarKey gradient picker, personalityTraits tags, isDefault transaction so only one default per recruiter; duplicate creates a clean copy.
    • RecruiterNote shipped with edit + delete bonus operations the original brief did not promise.
    • InterviewReport relational model keyed by candidateId — transcripts, scoring breakdown, strengths, weaknesses, recruiter notes all live in one row. Agent posts on call-end via the shared-secret callback.
    • EmailDispatch logs every Resend send with status (queued / sent / failed / bounced) + providerId; resendEmail mints a new row distinct from the original.
    • PendingAutomationDispatch approval queue persists awaiting_approval rows with snapshot fields (rule name, candidate name) so the audit log outlives deletes; recruiter approval / rejection workflow ships with sandboxed iframe previews.
    • CandidateFeedbackToken on Prisma with expiresAt + revokedAt; expired or revoked tokens return null end-to-end.
    • Integration directory + IntegrationNotifyRequest waitlist on DB; connectedAt and config survive restarts; FK satisfied so notify-me requests no longer 500.
    • Analytics tiles + weekly chart now derive from prisma.count + date_trunc raw queries against real Candidate + Campaign rows; no MOCK_ANALYTICS singleton.
    • 56 integration tests across 11 files round-trip every entity against live Neon in ~190s. CI gate `pnpm test:int` green on every push.
    • docs/plan/data-layer-go-real.md shipping log + docs/plan/contracts/endpoints.md updated to reflect 10/10 user-spec gaps complete. Two follow-up tasks (#25 MOCK_AUTOMATION_ACTIVITY, #26 MOCK_AUTOMATIONS legacy templates) tracked separately.

    Commits: 1233d1d, 9c1a1cf, 3e10dee, 8a24ad9, 9c8a5ea, d10da61, 467e1f6, 533092b, 0b5c33b

  16. Feature

    Pricing v3 — packs, monthly caps, no-shows free

    Pricing rebuilt for fairness on both sides. $9-per-interview retired in favour of pre-paid packs and monthly plans with a transparent cap. An interview only counts when the candidate audibly speaks.

    • Free tier unchanged: 3 lifetime interviews, no card.
    • Per-interview replaced with two pre-paid packs — Starter $19/10 ($1.90 effective) and Growth $79/50 ($1.58 effective), each with a 12-month expiry.
    • Monthly tiers gain a hard cap + transparent overage: Pro $49/mo for 30 interviews ($1.50 overage), Agency $149/mo for 100 interviews ($1.00 overage, white-label, 10 seats).
    • "No-shows free" applied across every tier — an interview only counts when the candidate has audibly spoken and the AI has asked at least one question.
    • Packs are non-refundable; materially-broken interviews credited back on request. Pack credits draw down before any monthly cap is consumed.
    • Terms § 8 fees rewritten to spell out the counting rule, pack expiry, no-refund policy, and cancellation/upgrade behaviour.

    Commits: c566db3

  17. FeatureInfra

    Recruiter ops loop — jobs board, automations, observer mode, shortlist tabs, feedback

    Five recruiter-side capabilities landed in a single push: a public jobs board, an automation execution engine with manual approval, observer mode for live interviews, shortlist preset tabs on the candidate table, and a candidate-facing feedback page.

    • Public `/jobs` board lists every published campaign with filters (department, employment type, location type); `/jobs/[slug]` renders the role detail with JobPosting JSON-LD for Google Jobs eligibility, validThrough = publishedAt + 60 days.
    • Automation execution engine (`lib/automation/dispatcher.ts` + `condition-eval.ts`) is idempotent per rule+candidate+trigger; auto rules fire immediately, manual rules push to `/automations/pending` for recruiter approval. Email body preview renders in a sandboxed iframe so untrusted recruiter edits can’t break out.
    • Recruiter observer mode: `/live` indexes in-progress interviews; `/live/[sessionId]` joins the candidate’s LiveKit room with a read-only token (canPublish=false, canSubscribe=true). 1-hour TTL, identity `observer_<userId>_<timestamp>`.
    • Shortlist preset tabs above the candidate table: All / Shortlist / Needs review / Live now. Shortlist = `recommendation ∈ {strong_yes, yes}` and not rejected. Bulk Advance / Reject / Send email floating bar appears on selection.
    • Candidate-facing `/feedback/[token]`: 30-day token-scoped public page showing strengths, weaknesses (gated by rule toggle), and AI-generated improvement tips. Marked noindex/nofollow.
    • `/api/ai/generate-tips` adds Gemini-backed tips with a canned fallback when `GOOGLE_GENERATIVE_AI_API_KEY` is missing.
    • Foundation types extended: `Campaign` now carries `publicSlug`, `publishedAt`, `location`, `comp`, `employmentType`, `seniority`, `department`, `requirements`. New types for pending dispatches, feedback payloads/tokens, observer tokens, and live session summaries. `EmailKind` adds `candidate_feedback`.

    Commits: 29c3a2e, 0a4e903, 61c6be8, ff533d0, 7085a20

  18. FixInfra

    Auth hardening — middleware closes `/live`, email-template XSS regression suite

    Two hardening passes on the auth surface. The recruiter observer pages are now session-gated at the edge; auth-related email templates escape every user-controlled field, with a vitest suite that locks the behaviour in.

    • Middleware matcher extended with `/live` and `/live/:path*` so unauthenticated visitors hit the existing redirect-to-login flow before any candidate names or job titles render.
    • `lib/email/auth-templates.ts` now escapes every user-controlled field through `escapeHtml`; verify and reset URLs flow through `escapeUrl`, which rewrites `javascript:` / `data:` / `vbscript:` schemes to `#`.
    • New `lib/auth-validators.ts` exposes `isValidEmail`, `isValidPassword`, `isValidName`, and `validateSignup` — `EMAIL_REGEX` matches the WHATWG `<input type="email">` standard, hard 254-char cap per RFC 5321.
    • 68 unit tests + 19 curl smoke checks cover OWASP-five XSS, the three blocked URL schemes, mailto/tel pass-through, and the signup validator order.

    Commits: 5a40ab1, 75de80a

  19. DesignFix

    GTM Wave 3.3 — vs/* + use-cases/* + product/* pages shipped, build unblocked

    Wave 3.3 of the marketing IA lands: vs/* competitor pages, use-cases/* segment pages, product/* deep-dives. Build went green after a prettier + escape-entity sweep across 14 files.

    • New landing FAQ anchor `faq-ll144` so the `/product/compliance` CTA can deep-link straight to the LL144 row.
    • Prettier + ESLint sweep across 14 wave-3.3 surfaces; two `&apos;` escapes in vs/hireflix + vs/karat; five unused lucide icons pruned. Marketing tree clean again.

    Commits: 8458f09, 6ab019a

  20. Design

    Footer refresh

    Marketing footer rebuilt around what visitors actually click — Product, Legal, and a "For recruiters" column — replacing the LiveKit byline.

    • Dropped the "Built on" column (LiveKit / LiveKit Agents / Next.js / Prisma) — internal stack detail that didn’t earn footer space.
    • Dropped the standalone "Compliance" column; AI compliance now sits inside the new Legal column with Privacy, Terms, Cookies, and Data.
    • New "For recruiters" column with Sign in, Dashboard, Help, and Support — the routes a visitor most likely needs after Product.
    • Bottom strip credit changed from "Voice on LiveKit" to a link pointing at vettika.com.

    Commits: cb2774c

  21. FeatureDesign

    Better Auth Phase 4 — sign-up gate, intent-aware login, last-used email memory

    The login flow now distinguishes sign-up from sign-in, remembers the last signed-in account in the browser, and surfaces a friendly prompt when someone tries to sign in without an account.

    • Login page renders two distinct buttons — "Sign in with Google" and "Create account with Google" — each writing a short-lived `auth_intent` cookie before redirecting to Google.
    • `databaseHooks.user.create.before` blocks brand-new User rows when the cookie says `signin`, throwing `UNAUTHORIZED` so Better Auth aborts the callback.
    • Failed sign-ins land on `/login?error=no_account` with an inline "No account found — create one?" prompt instead of a generic error.
    • AvatarMenu persists the active user (`{ email, name, image }`) to localStorage under `recruiter:last-user`; the login page reads it on mount and offers a "Continue as <name>" card with the Google avatar.
    • Middleware matcher doubled to include both `/path` and `/path/:path*` for every protected prefix so exact path hits can’t slip past.
    • Repo README rewritten as an Vettika quickstart (clone → install → env → db push → dev); upstream LiveKit starter README kept below as reference.

    Commits: 69b9d1f

  22. FeatureInfra

    Better Auth Phase 3 — sign-in is live

    End-to-end Google OAuth on top of the Phase 2 schema. Marketing and candidate routes stay open; recruiter routes are now session-gated at the edge.

    • `/api/auth/[...all]` catch-all Better Auth handler via `toNextJsHandler`.
    • `lib/auth-client.ts` exposes `signIn` / `signOut` / `useSession` / `getSession` from `better-auth/react`.
    • New `(auth)/login` page with a Google sign-in button that respects `?next=` for post-login redirect and surfaces OAuth errors inline.
    • Edge-safe middleware (`getSessionCookie`) gates `/dashboard`, `/account`, `/campaigns`, `/automations`, `/settings`, `/sessions`, `/interviews`. 33 kB bundle.
    • AvatarMenu dropped the demo recruiter stub and pulls live `name` / `email` / `image` from `useSession`; sign-out now calls `authClient.signOut` so the server session cookie is actually cleared.

    Commits: e145285

  23. Infra

    Better Auth Phase 2 — Prisma rename + auth schema

    Made room for Better Auth on the Prisma side without losing a single row in production. The old `Session` model is now `Interview`, table name preserved via `@@map`; four new auth tables (`user` / `session` / `account` / `verification`) joined the schema.

    • `model Session` → `model Interview` with `@@map("Session")` so the existing Neon table data stays exactly where it is.
    • Better Auth canonical models generated via `@better-auth/cli` — `User`, `Session`, `Account`, `Verification` — mapped to lowercase tables.
    • 16 call sites swept from `db.session.*` to `db.interview.*` across app routes, API routes, and helpers; `lib/sessions.ts` aliases `Interview as PrismaSession` so `SessionDTO` / `toDTO` stay untouched in this phase.
    • `lib/auth.ts` introduces `betterAuth()` with the Prisma adapter and Google provider — instance live, route handler arrives in Phase 3.

    Commits: 57125cd

  24. FeatureDesignInfra

    Route groups + settings split + candidate invite share

    Settings now lives in per-section routes with a responsive nav, every candidate row can share its own interview link, and the App Router tree is reorganized into clearly named groups.

    • Each settings section is its own route: Workspace, Notifications, Data & privacy, Danger zone — alongside the existing AI profiles, Integrations, and Billing pages.
    • New shared settings shell wraps every sub-route with a sticky sidebar on large screens and a horizontally scrollable underlined tab strip on mobile/tablet.
    • Each candidate row now has an inline "copy invite link" icon and a "Share invite…" dialog with a prefilled mailto body (first name, campaign title, scheduled slot) and a QR placeholder.
    • App Router tree split into URL-transparent `(marketing) / (app) / (candidate)` groups — no paths changed.
    • Recruiter AI generate routes (`/api/ai/generate-{questions,criteria,copy}`) backed by `@ai-sdk/google` ship behind the wizard.
    • Resend email backend (`/api/email/send` + `lib/email/templates.ts`) is wired and idle until `RESEND_API_KEY` is set.
    • Inline editable scoring criteria + required questions added to `/campaigns/[id]?edit=1`.
    • Marketing: pricing page, landing rebuild with rotating headline, demo widget, and transcript loop; SEO baseline (`robots.ts`, `sitemap.ts`).

    Commits: f43cfed, d39895e, dcb9842

  25. Infra

    Production domain + analytics

    Vettika is live at airecruiter.nexprove.com with TLS, with `@vercel/analytics` wired in the root layout. Lint warnings across the agents-ui and ai-elements scaffolds were cleaned up.

    • Custom domain `airecruiter.nexprove.com` issued and serving production.
    • `@vercel/analytics` enabled in `app/layout.tsx`.
    • Lint pass across `components/agents-ui` and `components/ai-elements`.
    • `PROTOTYPE_STATUS.md` updated to reflect session 3 deploy state.

    Commits: a55be45, 74ef348, 760e6de

  26. FeatureDesign

    Recruiter + candidate prototype IA

    End-to-end recruiter dashboard, campaigns, settings, automations, and candidate flow shipped against a mock API layer so we can iterate on the IA before wiring real endpoints.

    • Recruiter dashboard, campaigns list + detail, automations, sessions, interviews — all driven by `web/lib/api.ts` mocks.
    • Candidate flow: `/i/[token]` consent → system check → live interview → done/error.
    • Public apply link card + per-link interview slot scheduling.
    • 15 follow-up tasks captured in `web/PROTOTYPE_STATUS.md`.

    Commits: bd83c81

  27. DesignDocs

    Rebrand to Vettika

    "AI Interviewer" → "Vettika" across owned surfaces — landing, headers, footers, metadata, and copy.

    • Brand name applied to header, footer, page titles, and marketing copy.
    • Vettika brand linked from app footer.

    Commits: 6bfe69b

  28. FeatureDesign

    /legal hub + footer split

    New /legal landing with 7 policy sub-pages, and a footer split — rich `SiteFooter` for marketing routes vs compact `AppFooter` for app routes.

    • Pages: privacy, terms, cookies, data, AI compliance, help, support.
    • Marketing routes render `SiteFooter`; app routes render `AppFooter`.

    Commits: 551e5db

  29. FeatureInfra

    Candidate prejoin + Neon Postgres

    Candidates run a mic + name check before the call connects. The token route is hardened and the DB moved to Neon Postgres on Vercel.

    • `/i/[token]/check` mic + camera + name capture before connecting to LiveKit.
    • `/api/token` is session-scoped; dev-only guard removed.
    • Prisma datasource on Postgres (Neon marketplace integration).

    Commits: 5b267ec, 10b0a51, 7ee9e8b, 51068f0

  30. Infra

    Initial commit

    AI Interviewer scaffold — Next.js App Router + LiveKit Agents Python runner + Prisma SQLite, deployed on Vercel.

    • Next.js 15 + App Router on Vercel.
    • LiveKit Cloud + Agents (Deepgram STT, Gemini LLM, Cartesia TTS).
    • Python interviewer agent at `agent/interviewer.py`.

    Commits: d2c4b8e

Want to follow along?

Source lives on GitHub. Every entry above maps to one or more commits.

Pricing
Color scheme toggle
Changelog — Vettika