Help us shape the future of Go! Take the 2026 Go Developer Survey

waitlist

package
v1.9.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 33 Imported by: 0

README

waitlist

Gates signup behind an approval queue, for the stretch before you open the doors.

People join with an email and land in pending. You approve or reject from the admin routes, and only approved addresses can complete signup. The gate runs on BeforeSignUp, so it holds regardless of which auth method someone tries.

There is a stats endpoint, which is mostly there because everyone building a waitlist wants to know how long it is.

Use it when

  • You are pre-launch and capacity is the constraint, not demand
  • You want scarcity as part of the launch, with invites going out in batches
  • You are onboarding design partners by hand and a stray public signup would be awkward

Skip it when

  • You are live and growing. A gate nobody remembers turning on will quietly refuse real signups
  • You already gate by email domain. password.allowed_domains does that with no queue to manage
  • Nobody is going to work the queue. An unapproved list is worse than no list

Wiring

authsome.WithPlugin(waitlist.New()),

Enabled controls whether the gate actually blocks. Turning it off leaves the endpoints up so you can keep collecting emails without refusing signups.

With waitlist.enabled in client config, the React and Flutter signup forms check approval when you enter an email and press Continue. An approved address opens the signup details. A new address joins the queue, and pending entries show the waitlist message. Rejected entries show that access hasn't been approved. You can check again or use another email from either message.

The forms use the idempotent join endpoint, which returns an existing entry without changing its status or sending another join notification. Failed checks keep you on the current step for retry. The server checks approval again when you submit signup.

Config

Field Type Default What it does
Enabled bool false When true, signup requires a prior approved entry

Endpoints

Public routes are under {basePath}/waitlist, admin routes under {basePath}/admin/waitlist.

Method Path Purpose
POST /join Join the waitlist
GET /status Check an address's position and status
GET `` List entries (admin)
GET /stats Counts by status (admin)
POST /:entryId/approve Approve an entry (admin)
POST /:entryId/reject Reject an entry (admin)
DELETE /:entryId Delete an entry (admin)

Lifecycle hooks

Hook What happens
BeforeSignUp Refuses signup when the address has no approved entry

It also registers a NotificationMappingContributor, so approval and rejection can trigger messages through the notification plugin, and a MigrationProvider for its table.

notification or email deliver the "you're in" message. password can restrict by domain if that is the only gate you need.

Documentation

Overview

contract.go: Wires the waitlist plugin into the forge-dashboard contract surface via plugin.ContractContributor. The subpackage declares its own EntryRow / EntryQuery / EntryListRows / WaitlistStore interface (cycle avoidance); this file adapts the plugin's concrete Store via a small shim.

Package waitlist provides a pre-launch waitlist gate for AuthSome.

The waitlist plugin allows administrators to gate sign-ups behind an approval process. Users join the waitlist with their email address and are assigned a "pending" status. Administrators can approve or reject entries. Only approved users are allowed to complete the sign-up flow.

Usage:

eng, _ := authsome.NewEngine(
    authsome.WithStore(store),
    authsome.WithPlugin(waitlist.New()),
)

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNotFound is returned when a waitlist entry is not found.
	ErrNotFound = errors.New("waitlist: entry not found")

	// ErrDuplicateEmail is returned when the email is already on the waitlist.
	ErrDuplicateEmail = errors.New("waitlist: email already on waitlist")
)

Sentinel errors.

View Source
var MongoMigrations = migrate.NewGroup("authsome-waitlist", migrate.DependsOn("authsome"))

MongoMigrations is the MongoDB migration group for the waitlist plugin. MongoDB is schemaless so no actual migrations are needed.

View Source
var PostgresMigrations = migrate.NewGroup("authsome-waitlist", migrate.DependsOn("authsome"))

PostgresMigrations is the postgres migration group for the waitlist plugin.

View Source
var SqliteMigrations = migrate.NewGroup("authsome-waitlist", migrate.DependsOn("authsome"))

SqliteMigrations is the SQLite migration group for the waitlist plugin.

Functions

This section is empty.

Types

type ApproveRequest

type ApproveRequest struct {
	EntryID string `path:"entryId" description:"Waitlist entry ID"`
	Note    string `json:"note,omitempty" description:"Optional admin note"`
}

ApproveRequest binds the path + body for POST /admin/waitlist/:entryId/approve.

type Config

type Config struct {
	// Enabled controls whether the waitlist gate is active.
	// When true, sign-ups require a prior approved waitlist entry.
	Enabled bool
}

Config configures the waitlist plugin.

type DeleteRequest

type DeleteRequest struct {
	EntryID string `path:"entryId" description:"Waitlist entry ID"`
}

DeleteRequest binds the path param for DELETE /admin/waitlist/:entryId.

type JoinRequest

type JoinRequest struct {
	Email string `json:"email" description:"Email address to add to the waitlist"`
	Name  string `json:"name,omitempty" description:"Display name (optional)"`
	AppID string `json:"app_id,omitempty" description:"Application ID (optional, resolved from context if absent)"`
}

JoinRequest binds the body for POST /waitlist/join.

type ListRequest

type ListRequest struct {
	AppID  string `query:"app_id" description:"Filter by application ID"`
	Email  string `query:"email" description:"Filter by email address"`
	Status string `query:"status" description:"Filter by status (pending, approved, rejected)"`
	Cursor string `query:"cursor" description:"Pagination cursor"`
	Limit  int    `query:"limit" description:"Maximum number of results (default 50, max 200)"`
}

ListRequest binds query params for GET /admin/waitlist.

type MemoryStore

type MemoryStore struct {
	// contains filtered or unexported fields
}

MemoryStore is an in-memory waitlist store for development and testing.

func NewMemoryStore

func NewMemoryStore() *MemoryStore

NewMemoryStore creates an in-memory waitlist store.

func (*MemoryStore) CountByStatus

func (s *MemoryStore) CountByStatus(_ context.Context, appID id.AppID) (pending, approved, rejected int, err error)

func (*MemoryStore) CreateEntry

func (s *MemoryStore) CreateEntry(_ context.Context, e *WaitlistEntry) error

func (*MemoryStore) DeleteEntry

func (s *MemoryStore) DeleteEntry(_ context.Context, entryID id.WaitlistID) error

func (*MemoryStore) GetEntry

func (s *MemoryStore) GetEntry(_ context.Context, entryID id.WaitlistID) (*WaitlistEntry, error)

func (*MemoryStore) GetEntryByEmail

func (s *MemoryStore) GetEntryByEmail(_ context.Context, appID id.AppID, email string) (*WaitlistEntry, error)

func (*MemoryStore) ListEntries

func (s *MemoryStore) ListEntries(_ context.Context, q *WaitlistQuery) (*WaitlistList, error)

func (*MemoryStore) UpdateEntryStatus

func (s *MemoryStore) UpdateEntryStatus(_ context.Context, entryID id.WaitlistID, status WaitlistStatus, note string) error

type MongoStore

type MongoStore struct {
	// contains filtered or unexported fields
}

MongoStore implements waitlist.Store using the Grove MongoDB driver.

func NewMongoStore

func NewMongoStore(db *grove.DB) *MongoStore

NewMongoStore creates a new MongoDB-backed waitlist store.

func (*MongoStore) CountByStatus

func (s *MongoStore) CountByStatus(ctx context.Context, appID id.AppID) (pending, approved, rejected int, err error)

func (*MongoStore) CreateEntry

func (s *MongoStore) CreateEntry(ctx context.Context, e *WaitlistEntry) error

func (*MongoStore) DeleteEntry

func (s *MongoStore) DeleteEntry(ctx context.Context, entryID id.WaitlistID) error

func (*MongoStore) GetEntry

func (s *MongoStore) GetEntry(ctx context.Context, entryID id.WaitlistID) (*WaitlistEntry, error)

func (*MongoStore) GetEntryByEmail

func (s *MongoStore) GetEntryByEmail(ctx context.Context, appID id.AppID, email string) (*WaitlistEntry, error)

func (*MongoStore) ListEntries

func (s *MongoStore) ListEntries(ctx context.Context, q *WaitlistQuery) (*WaitlistList, error)

func (*MongoStore) UpdateEntryStatus

func (s *MongoStore) UpdateEntryStatus(ctx context.Context, entryID id.WaitlistID, status WaitlistStatus, note string) error

type Plugin

type Plugin struct {
	// contains filtered or unexported fields
}

Plugin is the waitlist management plugin.

func New

func New(cfg ...Config) *Plugin

New creates a new waitlist plugin. An in-memory store is used by default. Use SetWaitlistStore to inject a persistent store.

func (*Plugin) MigrationGroups

func (p *Plugin) MigrationGroups(driverName string) []*migrate.Group

MigrationGroups returns the waitlist migration groups for the given driver.

func (*Plugin) Name

func (p *Plugin) Name() string

Name returns the plugin name.

func (*Plugin) NotificationMappings

func (p *Plugin) NotificationMappings() map[string]*plugin.NotificationMapping

NotificationMappings contributes waitlist notification templates.

func (*Plugin) OnBeforeSignUp

func (p *Plugin) OnBeforeSignUp(ctx context.Context, req *account.SignUpRequest) error

OnBeforeSignUp implements plugin.BeforeSignUp. When the waitlist is enabled, it blocks sign-ups unless the email has an approved entry on the waitlist.

func (*Plugin) OnInit

func (p *Plugin) OnInit(_ context.Context, engine plugin.Engine) error

OnInit captures engine capabilities.

func (*Plugin) RegisterContract

func (p *Plugin) RegisterContract(
	d *dispatcher.Dispatcher,
	reg contract.Registry,
	wreg contract.WardenRegistry,
	engine plugin.Engine,
) error

func (*Plugin) RegisterRoutes

func (p *Plugin) RegisterRoutes(router forge.Router) error

RegisterRoutes registers waitlist HTTP routes.

func (*Plugin) SetWaitlistStore

func (p *Plugin) SetWaitlistStore(s Store)

SetWaitlistStore allows direct waitlist store injection.

type PostgresStore

type PostgresStore struct {
	// contains filtered or unexported fields
}

PostgresStore implements waitlist.Store using the Grove ORM with PostgreSQL.

func NewPostgresStore

func NewPostgresStore(db *grove.DB) *PostgresStore

NewPostgresStore creates a new PostgreSQL-backed waitlist store.

func (*PostgresStore) CountByStatus

func (s *PostgresStore) CountByStatus(ctx context.Context, appID id.AppID) (pending, approved, rejected int, err error)

func (*PostgresStore) CreateEntry

func (s *PostgresStore) CreateEntry(ctx context.Context, e *WaitlistEntry) error

func (*PostgresStore) DeleteEntry

func (s *PostgresStore) DeleteEntry(ctx context.Context, entryID id.WaitlistID) error

func (*PostgresStore) GetEntry

func (s *PostgresStore) GetEntry(ctx context.Context, entryID id.WaitlistID) (*WaitlistEntry, error)

func (*PostgresStore) GetEntryByEmail

func (s *PostgresStore) GetEntryByEmail(ctx context.Context, appID id.AppID, email string) (*WaitlistEntry, error)

func (*PostgresStore) ListEntries

func (s *PostgresStore) ListEntries(ctx context.Context, q *WaitlistQuery) (*WaitlistList, error)

func (*PostgresStore) UpdateEntryStatus

func (s *PostgresStore) UpdateEntryStatus(ctx context.Context, entryID id.WaitlistID, status WaitlistStatus, note string) error

type RejectRequest

type RejectRequest struct {
	EntryID string `path:"entryId" description:"Waitlist entry ID"`
	Note    string `json:"note,omitempty" description:"Optional admin note"`
}

RejectRequest binds the path + body for POST /admin/waitlist/:entryId/reject.

type SqliteStore

type SqliteStore struct {
	// contains filtered or unexported fields
}

SqliteStore implements waitlist.Store using the Grove ORM with SQLite.

func NewSqliteStore

func NewSqliteStore(db *grove.DB) *SqliteStore

NewSqliteStore creates a new SQLite-backed waitlist store.

func (*SqliteStore) CountByStatus

func (s *SqliteStore) CountByStatus(ctx context.Context, appID id.AppID) (pending, approved, rejected int, err error)

func (*SqliteStore) CreateEntry

func (s *SqliteStore) CreateEntry(ctx context.Context, e *WaitlistEntry) error

func (*SqliteStore) DeleteEntry

func (s *SqliteStore) DeleteEntry(ctx context.Context, entryID id.WaitlistID) error

func (*SqliteStore) GetEntry

func (s *SqliteStore) GetEntry(ctx context.Context, entryID id.WaitlistID) (*WaitlistEntry, error)

func (*SqliteStore) GetEntryByEmail

func (s *SqliteStore) GetEntryByEmail(ctx context.Context, appID id.AppID, email string) (*WaitlistEntry, error)

func (*SqliteStore) ListEntries

func (s *SqliteStore) ListEntries(ctx context.Context, q *WaitlistQuery) (*WaitlistList, error)

func (*SqliteStore) UpdateEntryStatus

func (s *SqliteStore) UpdateEntryStatus(ctx context.Context, entryID id.WaitlistID, status WaitlistStatus, note string) error

type StatsRequest

type StatsRequest struct {
	AppID string `query:"app_id" description:"Application ID"`
}

StatsRequest binds query params for GET /admin/waitlist/stats.

type StatsResponse

type StatsResponse struct {
	Pending  int `json:"pending" description:"Number of pending entries"`
	Approved int `json:"approved" description:"Number of approved entries"`
	Rejected int `json:"rejected" description:"Number of rejected entries"`
	Total    int `json:"total" description:"Total number of entries"`
}

StatsResponse holds waitlist entry counts by status.

type StatusRequest

type StatusRequest struct {
	Email string `query:"email" description:"Email address to check"`
	AppID string `query:"app_id" description:"Application ID (optional)"`
}

StatusRequest binds query params for GET /waitlist/status.

type StatusResponse

type StatusResponse struct {
	Email  string         `json:"email" description:"Email address"`
	Status WaitlistStatus `json:"status" description:"Waitlist status (pending, approved, rejected)"`
}

StatusResponse is returned when checking waitlist status.

type Store

type Store interface {
	// CreateEntry adds a new waitlist entry.
	CreateEntry(ctx context.Context, e *WaitlistEntry) error

	// GetEntry returns a waitlist entry by ID.
	GetEntry(ctx context.Context, entryID id.WaitlistID) (*WaitlistEntry, error)

	// GetEntryByEmail returns a waitlist entry by app and email.
	GetEntryByEmail(ctx context.Context, appID id.AppID, email string) (*WaitlistEntry, error)

	// UpdateEntryStatus changes the status and optionally sets a note.
	UpdateEntryStatus(ctx context.Context, entryID id.WaitlistID, status WaitlistStatus, note string) error

	// ListEntries returns waitlist entries matching the query.
	ListEntries(ctx context.Context, q *WaitlistQuery) (*WaitlistList, error)

	// CountByStatus returns counts of entries grouped by status for an app.
	CountByStatus(ctx context.Context, appID id.AppID) (pending, approved, rejected int, err error)

	// DeleteEntry permanently removes a waitlist entry.
	DeleteEntry(ctx context.Context, entryID id.WaitlistID) error
}

Store persists and queries waitlist entries.

type WaitlistEntry

type WaitlistEntry struct {
	ID        id.WaitlistID  `json:"id"`
	AppID     id.AppID       `json:"app_id"`
	Email     string         `json:"email"`
	Name      string         `json:"name"`
	Status    WaitlistStatus `json:"status"`
	UserID    *id.UserID     `json:"user_id,omitempty"` // set after sign-up
	IPAddress string         `json:"ip_address"`
	Note      string         `json:"note"`
	CreatedAt time.Time      `json:"created_at"`
	UpdatedAt time.Time      `json:"updated_at"`
}

WaitlistEntry is a single record on the waitlist.

type WaitlistList

type WaitlistList struct {
	Entries    []*WaitlistEntry `json:"entries"`
	Total      int              `json:"total"`
	NextCursor string           `json:"next_cursor,omitempty"`
}

WaitlistList wraps a paginated list of waitlist entries.

type WaitlistQuery

type WaitlistQuery struct {
	AppID  id.AppID       `json:"app_id"`
	Email  string         `json:"email,omitempty"`
	Status WaitlistStatus `json:"status,omitempty"`
	Cursor string         `json:"cursor,omitempty"`
	Limit  int            `json:"limit,omitempty"`
}

WaitlistQuery holds filters for listing waitlist entries.

type WaitlistStatus

type WaitlistStatus string //nolint:revive // exported name stutter is intentional for clarity

WaitlistStatus represents the approval state of a waitlist entry.

const (
	// StatusPending means the entry is awaiting review.
	StatusPending WaitlistStatus = "pending"
	// StatusApproved means the entry has been approved for sign-up.
	StatusApproved WaitlistStatus = "approved"
	// StatusRejected means the entry has been rejected.
	StatusRejected WaitlistStatus = "rejected"
)

Directories

Path Synopsis
Package contract is the waitlist plugin's forge-dashboard contract surface.
Package contract is the waitlist plugin's forge-dashboard contract surface.
Package waitlisttest provides a backend-agnostic conformance suite for the waitlist.Store interface.
Package waitlisttest provides a backend-agnostic conformance suite for the waitlist.Store interface.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL