Expand description
cratefield-module-waitlist: per-product waitlist with confirm,
position, referral codes and status (issue #11, architecture section
6).
use cratefield_module_waitlist::Waitlist;
let module = Waitlist::new()
.products(["kontinuum", "undercover-rockstars"])
.confirm_ttl_days(7)
.referrals(true);Positions are atomic. Confirmation reads the product’s position
from a per-product counter on waitlist_position_lock that the
lock-taking UPDATE increments in the same statement, and that UPDATE
plus the flip and the referrer credit run in one
cratefield_core::Database::batch_atomic — atomic on D1, a
transaction on the sqlite and Postgres adapters — so concurrent
confirms of one product serialize on every engine and a UNIQUE
(product, position) index means a duplicate position cannot survive
even if the allocation raced (issues #20, #173, #126). Positions are
dense at assign time per product
and never recomputed on delete.
No enumeration (section 11): POST /v1/waitlist answers the same
202 {"ok":true} bytes whatever the row state; an unknown ref code
is silently ignored.
§cratefield-module-waitlist
Per-product waitlist for a Cratefield venture: POST /v1/waitlist
joins, a signed confirmation link assigns a dense per-product position,
referral codes credit the referrer, and a status endpoint shows the
entry’s place and share link.
use cratefield_module_waitlist::Waitlist;
let module = Waitlist::new()
.products(["kontinuum", "undercover-rockstars"])
.confirm_ttl_days(7)
.referrals(true);Joins may carry a free-form answers JSON object: .answers_schema(|answers| ..)
validates it whenever answers are present, .require_answers(true) rejects
joins without them, and the admin export carries the stored answers as a
quoted CSV column.
Position semantics: positions are per product, assigned densely at
confirm time from a per-product counter on waitlist_position_lock that
the lock-taking UPDATE increments, inside one all-or-nothing
Database::batch_atomic, so concurrent confirms of the same product
serialize on every engine (D1, SQLite, Postgres) and never share a
position; a UNIQUE(product, position) index backstops the allocation.
Positions are never recomputed when rows are deleted.
Confirmation mail is deferred, and the join never depends on it.
POST /v1/waitlist answers 202 {"ok":true} for every accepted join,
whatever the mailer does — deliberately, so the answer leaks nothing about
whether the address was already on the list (docs/SECURITY.md). The
confirmation mail is built on the request path but sent after the response
returns, through the request’s Defer port. With no mailer configured
(RESEND_API_KEY unset, or an unverified sending domain) the join still
succeeds: the entry is recorded pending, no mail goes out, and the
NotConfigured is only logged. There is no 503 on this path. A send
failure releases the hour-long send claim, so a later join retries the
mail. A form that has to offer a fallback address when mail is unreliable
must get it from somewhere other than the join response.
Register the mail templates in the venture’s src/lib.rs, in the
venture’s own style: they render through cratefield-mail-templates
(ADR 0030) with the MailTheme you give them. default_templates() renders
in a theme built from the venture’s core Brand and its MAIL_THEME config
instead; either way, venture overrides registered later win.
use cratefield_core::Harness;
use cratefield_mail_templates::{MailTheme, Palette};
use cratefield_module_waitlist::themed_templates;
let theme = MailTheme::new("Acme", "https://acme.test")
.logo("https://acme.test/assets/email/logo-64.png", "Acme")
.light(Palette::neutral_light().button("#0E1526"));
let builder = Harness::builder().templates(themed_templates(&theme));Emits waitlist.joined and waitlist.confirmed; pair with
cratefield-module-email-signup’s
.subscribe_on_waitlist_confirm(true) to mirror confirmed addresses
into the signup list — no crate dependency between the modules.
See docs/ARCHITECTURE.md section 6 (Waitlist) and section 11.
MIT. Built in the open for Cratefield, a Factory Zero venture.
Structs§
- Confirm
Mail Data - Typed data for
waitlist/confirm. The module also puts the resolved theme undertheme(seemt::attach_theme), so a venture’s own override template can render in the same style. - Confirmed
Mail Data - Typed data for
waitlist/confirmed(the “you’re in” mail). - Waitlist
- A per-product waitlist.
Functions§
- default_
templates - The module’s default templates, for
Harness::builder().templates(..). They render in the theme the module resolves for each mail: the venture’s coreBrand, with the deployment’sMAIL_THEMEconfig on top. Usethemed_templatesto compose the venture’s own theme. - themed_
templates - The module’s templates in
theme, the venture’s own style, forHarness::builder().templates(..). The deployment’sMAIL_THEMEconfig still applies on top.
Type Aliases§
- Answers
Schema - A venture’s validation for the free-form
answersfield of a join.Err(detail)becomes a400 validation-failedproblem.