Warteliste für ein neues SaaS

Warteliste für ein neues SaaS7 Min. Lesezeit

So baust du ein Beta-Anmeldeformular, das Bewerber vorab prüft

Ein Beta-Bewerbungsformular mit Auswahlfragen, die der Server durchsetzt, und ein Skript, das Bewerber bewertet und jede Kohorte aus der Liste auswählt.

Preet SutharGründer, Ikiform

Stell neben der E-Mail ein paar Auswahlfragen und lass das Formular-Backend sie durchsetzen, damit jede Bewerbung vollständig und vergleichbar ankommt. Wähle dann jede Kohorte mit einem kurzen Bewertungsskript aus der Liste aus, lade eine kleine Gruppe ein und lass alle anderen auf die nächste Runde warten.

Eine Warteliste schreibt am Launch-Tag allen. Eine Beta macht das Gegenteil: Du lässt zwanzig Leute rein, lernst von ihnen und lässt dann zwanzig weitere rein. Das ändert das Formular. Du brauchst Antworten, nach denen du sortieren kannst, nicht nur eine Adresse.

Was sich für eine Beta ändert

  • Es ist eine Auswahl. Du lädst jeweils wenige Leute ein, brauchst also eine Möglichkeit, Bewerber gegeneinander zu ranken. Geschlossene Auswahlfragen wie Rolle und Teamgröße lassen sich sauber sortieren. Freitext nicht.
  • Unvollständige Bewerbungen kosten einen Platz. Wenn jemand die Frage überspringt, die dir sagt, ob die Person passt, kannst du sie weder auswählen noch aussortieren. Mach die Auswahlfragen zu Pflichtfeldern und lass den Server Posts ablehnen, die sie überspringen.
  • Einladungen gehen in Runden raus. Deine Liste muss sich merken, wen du schon eingeladen hast, damit die nächste Kohorte diese Leute überspringt. Ikiform hat keine Tags, dieser Zustand lebt also in deinem Skript oder deiner Tabelle.
  • Wer nicht ausgewählt wurde, zählt trotzdem. Das ist deine nächste Kohorte. Sag den Leuten in der Dankeszeile, dass Gruppen nach und nach eingeladen werden, damit ein stilles Postfach nicht wie eine Absage wirkt.

Auswahlfragen

Sechs Fragen sind die Obergrenze. Jede sollte ändern, wen du einlädst; wenn du nicht sagen kannst, wie eine Antwort jemanden nach oben oder unten bringt, streich sie. Probier unten das Formular aus. Es nutzt dieselbe Felddefinition, die Ikiform auf dem Server durchsetzt, du siehst also den 422, den eine unvollständige Bewerbung bekommt.

Vorschau

Führt den Validator von Ikiform in deinem Browser aus. Es wird nichts gesendet.

Teamgröße

Ein, zwei Sätze zu der Aufgabe, die du ihm am ersten Tag geben würdest.

Füge diese Definition im Tab Fields des Formulars ein. Die Antwort use_case braucht mindestens 40 Zeichen, was Bewerbungen mit einem Wort aussiebt.
FeldWas es entscheidetWarum dieser Typ
emailWen du einlädstPflicht. Eine geschäftliche E-Mail verrät die Firma, ohne zu fragen
roleOb sie das Problem spüren, das du löstEin select mit deinen eigenen Kategorien, damit du sie gewichten kannst
team_sizeOb deine erste Version zu ihrem Setup passtEin radio: eine Antwort, immer vergleichbar
use_caseOb sie die Beta diesen Monat nutzen werdenEine textarea mit Mindestlänge. Das ehrlichste Signal im Formular
current_toolWen du ablöst und womit sie dich vergleichen werdenOptionaler Freitext
interviewMit wem du sprechen kannstEine checkbox. Fehlt sie, gilt false, sie blockiert also nie eine Bewerbung
Mit einer Felddefinition lehnt Ikiform jedes unbekannte Feld mit 422 und einem unknown_field-Fehler ab. Wenn du ein verstecktes Eingabefeld wie ref hinzufügst, nimm es auch in die Definition auf.

Das Formular

Jede Version unten zeigt die Meldung des Servers unter dem Feld, zu dem sie gehört, weil ein Auswahlformular aus mehr Gründen scheitert als an einer falschen E-Mail. Jede behält, was die Person eingegeben hat, wenn ein Post abgelehnt wird, damit niemand einen Absatz neu tippt.

Natives required und minlength fangen die meisten Lücken vor dem Post ab. Das einfache Formular funktioniert ohne Skript und landet auf deiner Dankeseite; das Skript hält die Leute auf der Seite und listet jeden Feldfehler auf.

beta.html
<form id="beta-form" action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="email">Work email</label>
  <input id="email" name="email" type="email" autocomplete="email" required>

  <label for="role">Your role</label>
  <select id="role" name="role" required>
    <option value="">Choose one</option>
    <option value="founder">Founder</option>
    <option value="engineer">Engineer</option>
    <option value="product">Product manager</option>
    <option value="designer">Designer</option>
    <option value="other">Something else</option>
  </select>

  <fieldset>
    <legend>Team size</legend>
    <label><input type="radio" name="team_size" value="1" required> Only me</label>
    <label><input type="radio" name="team_size" value="2-10"> 2 to 10</label>
    <label><input type="radio" name="team_size" value="11-50"> 11 to 50</label>
    <label><input type="radio" name="team_size" value="51+"> More than 50</label>
  </fieldset>

  <label for="use_case">What would you use it for first?</label>
  <textarea id="use_case" name="use_case" rows="4" minlength="40" maxlength="1000" required></textarea>

  <label for="current_tool">What do you use today?</label>
  <input id="current_tool" name="current_tool" maxlength="120">

  <label><input type="checkbox" name="interview" value="true"> I'm open to a 20-minute call</label>
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden>
  <ul id="beta-errors" role="alert"></ul>
  <button>Apply for the beta</button>
</form>
Vor </body>
<script>
  const form = document.getElementById("beta-form");
  const errors = document.getElementById("beta-errors");
  const labels = { email: "Work email", role: "Role", team_size: "Team size", use_case: "First use", current_tool: "Current tool" };

  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    const button = form.querySelector("button");
    button.disabled = true;
    errors.replaceChildren();
    try {
      const response = await fetch(form.action, {
        method: "POST",
        headers: { Accept: "application/json" },
        body: new FormData(form),
      });
      const result = await response.json();
      if (result.ok) {
        form.outerHTML = "<p role=\"status\">Thanks for applying. We invite testers in small groups and will email you either way.</p>";
        return;
      }
      const fields = Object.entries(result.error.fields ?? {});
      const lines = fields.length ? fields.map(([id, messages]) => `${labels[id] ?? id}: ${messages[0]}`) : [result.error.message];
      errors.replaceChildren(...lines.map((line) => Object.assign(document.createElement("li"), { textContent: line })));
    } catch {
      errors.textContent = "Couldn't reach the server. Your answers are still here; try again.";
    }
    button.disabled = false;
  });
</script>
Die serverseitigen Versionen senden mit einem Secret Key, daher überspringt Ikiform Allowed domains und seine Menschlichkeitsprüfung. Jeder Post kommt dann von der IP deines Servers, und das Standard-Limit von 5 pro Minute und Besucher zählt alle deine Besucher zusammen. Wähle für ein Formular, das du über einen Server sendest, das Rate-Limit Relaxed oder Custom, und halte IKIFORM_SECRET_KEY in reinen Server-Umgebungsvariablen.

Eine Kohorte auswählen

Wie aus Bewerbungen Beta-Kohorten werdenBewerbungenvollständig,vom ServererzwungenbewertenRolle,Größe,Aufwand,GesprächTop 25Kohorte 1,eingeladennächsteRundeEingeladeneüberspringenwartennochwissen, dassGruppenfolgenExportzwei Wochenunter der GrenzeWie aus Bewerbungen Beta-Kohorten werdenBewerbungenvollständig, vomServer erzwungenbewertenRolle, Größe, Aufwand,GesprächTop 25Kohorte 1, eingeladennächste RundeEingeladeneüberspringenwartennochwissen, dassGruppenfolgenExportzwei Wochenunter der Grenze
  1. Bewerbungen kommen vollständig an, weil Ikiform Posts ablehnt, die eine Pflichtfrage überspringen.
  2. Ein Skript liest sie über die API und bewertet jede nach Rolle, Teamgröße, Länge des Anwendungsfalls und der Gesprächs-Checkbox.
  3. Die besten 25 werden die erste Kohorte und bekommen eine Einladung aus deinem E-Mail-Tool.
  4. Führe das Skript zwei Wochen später erneut mit den eingeladenen E-Mails aus, damit sie übersprungen werden, und wähle die nächste Gruppe.
  5. Alle unter der Grenze bleiben auf der Liste und hören von dir, wenn ihre Runde kommt.
Scores sind eine Startreihenfolge, kein Urteil. Lies den Anwendungsfall aller, die nahe an der Grenze liegen, selbst.

Das Skript liest jede empfangene Bewerbung mit der REST-API, behält die neueste pro E-Mail, bewertet sie und schreibt die beste Gruppe in cohort.csv. Übergib die E-Mails, die du schon eingeladen hast, als Argumente, damit die nächste Runde sie überspringt. Es braucht einen Secret Key mit dem Scope submissions:read.

cohort.mjs
import { writeFileSync } from "node:fs";
import { createClient } from "@ikiform/sdk";

const ikiform = createClient({ apiKey: process.env.IKIFORM_SECRET_KEY });
const COHORT_SIZE = 25;
const invited = new Set(process.argv.slice(2).map((email) => email.toLowerCase()));

const ROLE = { founder: 3, engineer: 3, product: 2, designer: 1, other: 0 };
const SIZE = { "2-10": 3, "11-50": 2, "1": 1, "51+": 1 };

const people = new Map();
for await (const submission of ikiform.submissions.iterate({ formId: "frm_xxxxxxxxxxxxxxxx", status: "received", limit: 100 })) {
  const answers = submission.fields;
  const email = String(answers.email ?? "").trim().toLowerCase();
  if (!email || invited.has(email) || people.has(email)) continue;
  const words = String(answers.use_case ?? "").split(/\s+/).length;
  const score =
    (ROLE[answers.role] ?? 0) +
    (SIZE[answers.team_size] ?? 0) +
    Math.min(3, Math.floor(words / 25)) +
    (answers.interview === true ? 2 : 0) +
    (answers.current_tool ? 1 : 0);
  people.set(email, { email, score, role: answers.role, team_size: answers.team_size });
}

const ranked = [...people.values()].sort((a, b) => b.score - a.score);
const cohort = ranked.slice(0, COHORT_SIZE);
const quote = (value) => `"${String(value ?? "").replaceAll('"', '""')}"`;
const rows = cohort.map((person) => [person.email, person.score, person.role, person.team_size].map(quote).join(","));
writeFileSync("cohort.csv", ["email,score,role,team_size", ...rows].join("\n"));
console.log(`${ranked.length} applicants, ${cohort.length} in this cohort, lowest score ${cohort.at(-1)?.score}`);
  • Passe ROLE und SIZE daran an, für wen deine erste Version gedacht ist. Ein Team-Tool könnte 2-10 am höchsten bewerten; ein Solo-Tool 1.
  • Der Anwendungsfall bringt einen Punkt pro 25 Wörter, bis zu 3. Länge ist nicht Qualität, aber wer drei Sätze schreibt, taucht meist auch auf.
  • Jeden Wert in Anführungszeichen zu setzen verhindert, dass Kommas in Antworten die CSV kaputt machen.
  • Speichere die Liste der Eingeladenen an einem Ort, an dem du sie wiederfindest, etwa in einer invited.txt, die du mit $(cat invited.txt) übergibst.

Die Einladung senden

Importiere cohort.csv in dein E-Mail-Tool und sende die Einladung von dort. Ikiform schreibt der Person, die das Formular ausfüllt, keine E-Mail. Benachrichtigungs-E-Mails gehen nur an dich und dein Team, es gibt also keinen Autoresponder, keine Bestätigungs-E-Mail und kein Double-Opt-in. Sende diese aus deinem E-Mail-Tool, ausgelöst durch einen Webhook. Halte die erste Einladung kurz: was sie bekommen, das eine, was sie ausprobieren sollen, und wie sie dich erreichen. Jede Benachrichtigungs-E-Mail, die Ikiform dir sendet, hat ein Reply, das an den Bewerber geht, was zu einer kurzen persönlichen Nachricht an die Leute nahe der Grenze passt.

Prüfen, ob es funktioniert

  1. Füge unter Allowed domains des Formulars die Domain deiner Site hinzu, plus localhost, während du entwickelst. Lass das Feld leer, um Posts von überall anzunehmen.
  2. Füge die Felddefinition im Tab Fields des Formulars ein und speichere sie.
  3. Sende eine Bewerbung mit einem einzeiligen Anwendungsfall ab. Das Formular zeigt die Mindestlängen-Meldung unter diesem Feld, und nichts wird gespeichert.
  4. Sende eine vollständige Bewerbung ab. Sie erscheint unter Submissions mit jeder Antwort, und interview lautet true oder false.
  5. Prüfe das Postfach jedes Benachrichtigungsempfängers. Jeder bekommt eine E-Mail mit den Feldern, und Reply geht an den Absender, wenn das Formular ein email-Feld hat.
  6. Sende einen Post mit ausgefülltem Honeypot, _gotcha=bot. Er bekommt trotzdem eine Erfolgsantwort, aber die Einsendung wird als Spam gespeichert und löst weder E-Mail noch Webhook aus.
  7. Führe das Bewertungsskript mit einem Secret Key aus und prüfe, dass cohort.csv deine Testbewerbung mit ihrem Score enthält.

Stolperfallen

ProblemWas zu tun ist
422 validation_failedDer Post passt nicht zur Felddefinition des Formulars. error.fields nennt jedes Feld und den Grund. Gleiche den name des Eingabefelds an die Feld-ID an.
422 mit Unknown field für ref, utm_source oder ein anderes verstecktes EingabefeldDie Felddefinition listet es nicht auf. Füge es als optionales text-Feld hinzu oder benenne es mit einem führenden _ um, damit Ikiform es verwirft.
Eine lange Antwort verschwindet in Next.js nach einem FehlerReact setzt das Formular nach einer Server Action zurück. Gib die Antworten aus der Action zurück und setze defaultValue an jedem Eingabefeld, wie es das Next.js-Beispiel tut.
Das Bewertungsskript wirft 401 unauthorized oder 403 forbiddenEin 401 heißt, der Schlüssel fehlt oder wurde widerrufen; ein 403 heißt, er ist publishable oder hat den Scope nicht. Nutze einen Secret Key (ik_sk_) mit dem Scope submissions:read.
Die Einsendung ist im Dashboard, aber keine E-Mail und kein Webhook kam anSie wurde als Spam gespeichert. Spam löst nichts aus. Öffne sie unter dem Filter Spam, um die Gründe zu sehen, und markiere sie als kein Spam, um ihre E-Mail und Webhooks zu senden.

Fragen

Hängst du bei einem Schritt fest oder willst eine Anleitung für deinen Fall? Schreib an hello@ikiform.com.