O

ouhammmourachid / waitlist

Public

A production-ready PocketBase waitlist collection for collecting emails, assigning queue positions, tracking invitation status, and recording signup sources.

  • Auth
  • 1 deploy
  • 1 fork

Waitlist

A simple PocketBase waitlist collection for collecting emails, tracking queue positions, and managing invitations.

Features

  • ๐Ÿ“ง Collect email addresses
  • ๐Ÿ”ข Track waitlist position
  • ๐Ÿ“Š Manage user status
  • โœ‰๏ธ Track invitation date
  • ๐Ÿ”— Track signup source
  • ๐Ÿ• Automatic timestamps

Fields

FieldTypeDescription
emailEmailUser's email address
statusSelectpending, invited, converted
positionNumberUser's queue position
invited_atDateInvitation date
sourceTextSignup source
createdAutodateCreation date
updatedAutodateLast update date

Status

pending โ†’ invited โ†’ converted
  • pending โ€” User joined the waitlist.
  • invited โ€” Invitation was sent.
  • converted โ€” User joined the product.

API

Add to waitlist

POST /api/collections/waitlist/records
{
  "email": "user@example.com",
  "source": "landing_page"
}

Get entries

GET /api/collections/waitlist/records

Update a user

PATCH /api/collections/waitlist/records/{id}
{
  "status": "invited",
  "invited_at": "2026-09-28 17:30:00"
}

Recommended Rules

For a public waitlist form:

RuleValue
Listnull
Viewnull
Create""
Updatenull
Deletenull

This allows anyone to join the waitlist while preventing public users from viewing or modifying existing entries.

Queue Position

The position field can be automatically assigned using a PocketBase hook:

First user  โ†’ #1
Second user โ†’ #2
Third user  โ†’ #3

For production use, handle concurrent registrations carefully to avoid duplicate positions.

Example

await pb.collection("waitlist").create({
  email: "user@example.com",
  source: "landing_page"
});

Use Cases

Perfect for:

  • SaaS launches
  • Beta programs
  • Early access
  • Product launches
  • Startup landing pages
  • Private invitations

License

MIT

waitlist
  • id
  • email
  • statussingle
  • phone
  • position
  • invited_at
  • source
  • created
  • updated

1 collection ยท 0 relations

main.pb.js104 lines
onRecordCreateRequest((e) => {
    const email = e.record.get("email").trim().toLowerCase()
    e.record.set("email", email)

    // block duplicate signups
    try {
        const existing = e.app.findFirstRecordByFilter(
            "waitlist",
            "email = {:email}",
            { email }
        )
        if (existing) {
            throw new BadRequestError("This email is already on the waitlist.")
        }
    } catch (err) {
        if (err instanceof BadRequestError) throw err
        // no record found โ€” fine, continue
    }

    // assign next position in queue
    let count = 0
    try {
        count = e.app.countRecords("waitlist")
    } catch (err) {
        count = 0
    }
    e.record.set("position", count + 1)

    e.next()
}, "waitlist")

onRecordAfterCreateSuccess((e) => {
    const position = e.record.get("position")
    const email = e.record.get("email")
    const name = e.record.get("name") || ""

    const html = `
<!DOCTYPE html>
<html>
<body style="margin:0;padding:0;background-color:#f4f4f4;font-family:'Helvetica Neue',Arial,sans-serif;">
  <table width="100%" cellpadding="0" cellspacing="0" style="background-color:#f4f4f4;padding:40px 0;">
    <tr>
      <td align="center">
        <table width="480" cellpadding="0" cellspacing="0" style="background-color:#ffffff;border-radius:12px;overflow:hidden;">

          <!-- Header -->
          <tr>
            <td style="background-color:#0a0a0a;padding:40px 40px 32px 40px;text-align:center;">
              <div style="font-size:13px;letter-spacing:3px;color:#ffffff;text-transform:uppercase;font-weight:600;">Talla AI</div>
            </td>
          </tr>

          <!-- Body -->
          <tr>
            <td style="padding:40px;">
              <h1 style="margin:0 0 16px 0;font-size:24px;line-height:1.3;color:#0a0a0a;font-weight:700;">
                You're on the list${name ? `, ${name}` : ""}.
              </h1>
              <p style="margin:0 0 24px 0;font-size:15px;line-height:1.6;color:#4a4a4a;">
                Thanks for joining the Talla AI waitlist. You're currently
                <strong style="color:#0a0a0a;">#${position}</strong> in line.
              </p>

              <div style="background-color:#f7f7f7;border-radius:8px;padding:20px 24px;margin-bottom:24px;">
                <p style="margin:0;font-size:13px;line-height:1.6;color:#6a6a6a;">
                  Studio-quality fashion imagery, generated โ€” never photographed.
                  We'll email you the moment your spot opens up.
                </p>
              </div>

              <a href="https://www.tallaai.com/gallery" style="display:inline-block;background-color:#0a0a0a;color:#ffffff;text-decoration:none;font-size:14px;font-weight:600;padding:14px 28px;border-radius:6px;">
                Explore the Gallery โ†’
              </a>
            </td>
          </tr>

          <!-- Footer -->
          <tr>
            <td style="padding:24px 40px;border-top:1px solid #eeeeee;">
              <p style="margin:0;font-size:12px;color:#999999;">
                ยฉ 2026 Talla AI. All rights reserved.<br/>
                You're receiving this because you signed up at tallaai.com.
              </p>
            </td>
          </tr>

        </table>
      </td>
    </tr>
  </table>
</body>
</html>
`

    const message = new MailerMessage({
        from: { address: $app.settings().meta.senderAddress, name: $app.settings().meta.senderName },
        to: [{ address: e.record.get("email") }],
        subject: "You're on the Talla AI waitlist โœจ",
        html: html,
    })

    e.app.newMailClient().send(message)
    e.next()
}, "waitlist")