Skip to main content

Collect feedback in an Astro site

Accept feedback from inside your product, have each message classified, and forward it to wherever your team already works. No inbox to babysit.

Keys

Browser calls use a publishable key (nv_pub_live_…), which is safe to ship. Reading and admin calls use a secret key (nv_live_…) on your server only. Create both under Dashboard → API keys.

PUBLIC_NORDVA_PUBLISHABLE_KEY=nv_pub_live_…  # .env
NORDVA_SECRET_KEY=nv_live_…  # .env, server only

01Browser: feedback form (publishable key)

src/components/FeedbackForm.astro

---
// Rendered on the server; the script below runs in the browser.
const KEY = import.meta.env.PUBLIC_NORDVA_PUBLISHABLE_KEY;
---
<form id="feedback">
  <textarea name="text" required minlength="5" maxlength="2000"></textarea>
  <button type="submit">Send feedback</button>
  <p id="feedback-msg" role="status"></p>
</form>
<script define:vars={{ KEY }}>
  const form = document.getElementById("feedback");
  const msg = document.getElementById("feedback-msg");
  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    const text = new FormData(form).get("text");
    const res = await fetch("https://api.nordva.dev/v1/feedback", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ feedback_text: text, page_url: location.href }),
    });
    const { data, error } = await res.json();
    msg.textContent = error ? error.message : "Thanks, received (" + data.category + ").";
  });
</script>

Feedback is an Indie and Builder feature; on Free the API answers PLAN_UPGRADE_REQUIRED. Register the page's origin under Project → Allowed origins.

02Server: list open bugs, API route

src/pages/api/feedback-list.ts

import type { APIRoute } from "astro";

export const GET: APIRoute = async () => {
  const res = await fetch("https://api.nordva.dev/v1/feedback?category=bug&status=open&limit=20", {
    method: "GET",
    headers: {
      Authorization: `Bearer ${import.meta.env.NORDVA_SECRET_KEY}`,
    },
  });
  if (!res.ok) {
    const { error } = await res.json();
    throw new Error(`${error.code}: ${error.message}`);
  }
  const { data } = await res.json();
  // data is an array; page with the cursor in meta
  return new Response(JSON.stringify(data), { headers: { "Content-Type": "application/json" } });
};

03Zero-code alternative: hosted widget

any HTML page

<script src="https://cdn.nordva.dev/v1/feedback.js"
  data-key="nv_pub_live_…"
  data-position="bottom-right"
  data-accent="#A8552E"
  data-user-id="user_123"></script>

04Verify from a terminal

curl -s "https://api.nordva.dev/v1/feedback?limit=5" \
  -H "Authorization: Bearer nv_live_…" | jq '.data[] | {category, sentiment, feedback_text}'

Behaviour worth knowing

  • Every response is { data, error, meta }. On failure error.code is a stable string such as VALIDATION_ERROR, PLAN_LIMIT_REACHED or RATE_LIMITED, with a remediation message.
  • POST and PATCH requests accept an Idempotency-Key header; the same key with the same body returns the original response for 24 hours.
  • Rate limits per key: 30 requests a minute on Free, 120 on Indie, 500 on Builder. A 429 carries Retry-After.
  • Browser calls with a publishable key must come from an origin registered on the project, otherwise the API answers ORIGIN_NOT_ALLOWED.