Skip to main content

Crate cratefield_module_waitlist

Crate cratefield_module_waitlist 

Source
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 — A place in the queue.

cratefield-module-waitlist on crates.io cratefield-module-waitlist documentation MIT

§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§

ConfirmMailData
Typed data for waitlist/confirm. The module also puts the resolved theme under theme (see mt::attach_theme), so a venture’s own override template can render in the same style.
ConfirmedMailData
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 core Brand, with the deployment’s MAIL_THEME config on top. Use themed_templates to compose the venture’s own theme.
themed_templates
The module’s templates in theme, the venture’s own style, for Harness::builder().templates(..). The deployment’s MAIL_THEME config still applies on top.

Type Aliases§

AnswersSchema
A venture’s validation for the free-form answers field of a join. Err(detail) becomes a 400 validation-failed problem.