Add a waitlist in a Vue or Nuxt app
Collect signups before launch and turn them into users afterwards. Each signup gets a referral code, invites go out in first-come order, and the whole list exports as CSV.
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.
NUXT_PUBLIC_NORDVA_PUBLISHABLE_KEY=nv_pub_live_… # .env
NUXT_NORDVA_SECRET_KEY=nv_live_… # .env, server only 01Browser: signup form (publishable key)
components/WaitlistForm.vue
<script setup lang="ts">
import { ref } from "vue";
const KEY = useRuntimeConfig().public.nordvaPublishableKey;
const email = ref("");
const result = ref<null | { position: number; already: boolean; link: string }>(null);
const err = ref<string | null>(null);
async function submit() {
err.value = null;
const res = await fetch("https://api.nordva.dev/v1/waitlist/signups", {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email: email.value, referrer_code: new URLSearchParams(location.search).get("ref") ?? undefined }),
});
const { data, error } = await res.json();
if (error) { err.value = error.message; return; }
result.value = { position: data.position, already: data.already_registered, link: data.referral_link };
}
</script>
<template>
<p v-if="result">{{ result.already ? "Already on the list" : "You're in" }} — #{{ result.position }}. Share: <a :href="result.link">{{ result.link }}</a></p>
<form v-else @submit.prevent="submit">
<input v-model="email" type="email" required placeholder="[email protected]" />
<button type="submit">Join waitlist</button>
<p v-if="err" role="alert">{{ err }}</p>
</form>
</template> The publishable key is safe to ship to browsers. Register your site's origin under Project → Allowed origins first, or the request is refused with ORIGIN_NOT_ALLOWED.
02Server: read stats, Nuxt server route
server/api/waitlist-stats.get.ts
export default defineEventHandler(async () => {
const res = await fetch("https://api.nordva.dev/v1/waitlist/stats", {
method: "GET",
headers: {
Authorization: `Bearer ${useRuntimeConfig().nordvaSecretKey}`,
},
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${error.code}: ${error.message}`);
}
const { data } = await res.json();
// data.total_signups, data.confirmed_count, data.invited_count, data.top_referrers
return data;
});
// nuxt.config.ts: runtimeConfig: { nordvaSecretKey: "", public: { nordvaPublishableKey: "" } } 03Zero-code alternative: hosted widget
any HTML page
<script src="https://cdn.nordva.dev/v1/waitlist.js"
data-key="nv_pub_live_…"
data-placeholder="[email protected]"
data-button="Join waitlist"
data-success="You're on the list"
data-theme="auto"></script>
<nordva-waitlist></nordva-waitlist> 04Verify from a terminal
curl -s https://api.nordva.dev/v1/waitlist/stats \
-H "Authorization: Bearer nv_live_…" | jq .data Behaviour worth knowing
- Every response is
{ data, error, meta }. On failureerror.codeis a stable string such as VALIDATION_ERROR, PLAN_LIMIT_REACHED or RATE_LIMITED, with a remediation message. - POST and PATCH requests accept an
Idempotency-Keyheader; 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.