Status: draft
Since D4.1 (#412) the event schema is owned by the shared community_base.events app, installed
with EVENT_URL_STYLE="public_id"; the site ships no events Django app. The site keeps a thin
adapter over the shared row:
- the site's event identity UUID rides in the shared row's
content_id; the numeric public ID and the title slug are preserved across imports; content.EventSource(site table) holds the reviewed source triple and checksum that the content imports and the description bridge resolve by — the shared row has no columns for them;- the DTC event type, season, and episode ride in the shared row's tags; external links (recordings, recaps, course/cohort references) ride in the ordered materials list;
- speakers are written as package
Hostrows (kind="speaker",external_ref=<person short>, first-minted name wins across sites) and their public profile paths resolve at read time throughHOST_PROFILE_RESOLVERagainst the people catalogue; - package
status(upcoming/completed) replaces the site lifecycle at the row level; the site-sidedraft -> published -> completed -> archivedlifecycle below remains the registration and email semantics until the D4.2 cutover.
Public events are exposed at /events/<positive-public-id>/<current-title-slug>. Provider and
recording destinations appear only as clearly labelled safe external actions on the detail, and
every checked speaker key resolves to /people/<short>.html via the resolver.
- the site identity UUID (
content_id) and the shared row's immutable key; - separate stable, unique, positive numeric public ID, allocated collision-safely once, never reused or renumbered, and exposed by management only as read-only public metadata;
- cosmetic title-derived public slug; explicit aliases for approved renames are retired
(
/events/<id>/<slug>is the only public shape); - title, description (bridge-resolved), kind, platform, and visibility;
- timezone-aware start/end plus the event's IANA timezone;
- online/in-person location and separately protected join information;
- ordered
Hostrelationships for speakers and hosts, resolved to people profiles at read time; - the site-side provenance triple and checksum in
content.EventSource.
The public ID alone selects a public Event; its title slug is cosmetic. Publication fails closed without a public ID. Imports and replays preserve the checked UUID/public-ID pair and reject missing, duplicate, ambiguous, or renumbered mappings atomically. An Event is addressed publicly only by its id: the UUID/current-slug, UUID-only, and reviewed date/title spellings that once redirected to the numeric/current-slug canonical are retired and resolve to nothing. No public slug-only, date/title-derived, UUID, provider-ID, source-key, or numeric management lookup exists.
Row status: upcoming or completed on the shared row, derived at import from the site lifecycle
(published → upcoming, completed → completed). Registration availability is derived from publication,
registration window, event time, and cancellation state.
MVP has no capacity or waitlist. The schema and services must not imply unlimited capacity forever, but capacity workflows are deferred until their product rules are specified.
- Event speakers and hosts are package
Hostrows keyed byexternal_ref= the canonical GitHub-backed personshortkey; public profile paths resolve throughHOST_PROFILE_RESOLVERat read time, never stored on the row. - One person can hold multiple roles and can appear in any number of events, podcasts, articles, books, courses, or cohorts.
- Studio selects only resolvable active profiles and shows an edit-on-GitHub link.
- Removing a public person record is blocked while database events/courses still reference the key, unless an alias/replacement is supplied.
EventRegistration stores:
- UUID, event, original email, normalized email, optional display name and timezone;
- status, registration/reactivation version, created/verified/cancelled/attended timestamps;
- privacy-notice version and acknowledgement timestamp;
- separate optional newsletter-consent value, text version, source, and timestamp;
- safe acquisition metadata such as UTM fields;
- abuse metadata with deliberately short retention;
- optional future user relation.
There is one row per (event, normalized_email). Cancellation transitions the row; re-registration reactivates and increments its version. Database constraints, not only form checks, enforce uniqueness.
Event email is an optional preference category and is separate from newsletter/marketing preference. It is evaluated for every event message and only applies to a recipient with an active registration for that event. Turning it off suppresses verification, confirmation, cancellation, reschedule, reminder, and other event-lifecycle messages. The existing newsletter-consent field is not an event-mail preference and event registration does not infer marketing subscription.
pending_verification -> confirmed -> cancelled, with expired, attended, and no_show where applicable.
- An anonymous visitor submits the registration form.
- The service validates the event window, normalizes email, applies rate limits, and atomically
creates/reactivates a pending registration. If the event preference permits delivery, it also
creates one logical verification
EmailDeliveryintent and its durable job; if the preference suppresses event mail, no Relay job is created and the pending registration remains pending until another supported verification path is used. - The response is deliberately uniform whether the address was new, pending, confirmed, or rate-limited in a non-user-actionable way.
- A high-entropy, hashed, registration/version-scoped link verifies ownership and confirms the registration.
- Confirmation atomically creates its logical confirmation delivery intent, durable job, and calendar invitation idempotently when event mail is enabled for that recipient. Calendar invitation delivery is therefore also suppressed when the recipient has opted out of event email.
- A separate high-entropy management link opens a GET confirmation screen; cancellation itself requires POST and rotates the registration version.
Verification and management tokens are never stored in plaintext, written to logs, placed in analytics, or exposed through Studio/API responses.
- Rescheduling increments the calendar sequence, updates public display, and creates one update message per active registration.
- Cancelling an event closes registration, increments calendar sequence, and creates cancellation messages plus cancelled ICS content.
- Bulk notification commands are asynchronous, resumable, rate-aware, idempotent, and produce operator-visible results.
- Editing title/time/location after registrations exist requires a change summary and explicit notification decision.
- Events are archived, not physically deleted, once registrations or messages exist.
The target Relay-backed purpose catalog includes:
- course/cohort registration confirmation for an already verified durable member account, not an anonymous pending-course-registration verification flow;
- enrollment welcome/removal;
- deadline reminders;
- homework/project score notifications;
- peer-review assignment and completion reminders;
- course updates;
- certificate issue/revoke/reissue;
- account verification and password recovery.
Course/cohort message idempotency keys include cohort and relevant enrollment/assignment/submission versions so repeated jobs cannot send the same logical message twice.
The #22 purpose decision authorizes the catalog structurally; each purpose still requires its approved Relay routing, immutable template version, idempotency inputs, and recipient preference check before submission. Unknown purposes, senders, broad recipients, and incomplete routing fail closed.
Datamailer is retired. D1.2ca deleted the client, its templates registry and its operator surfaces; D1.2cb dropped the outbox, send-audit, dispatch-run and contact-event storage. No website code path reaches it, it receives no send, and it cannot become a rollback sender. External identifiers carried over from it survive only inside the records that already hold them.
Marketing/newsletter delivery is in MVP scope as a distinct optional purpose. New member/account signup defaults the marketing preference on, but unverified new accounts are not newsletter recipients. The recipient can unsubscribe at any time.
For legacy/imported contacts, Mailchimp is authoritative for the marketing preference. Importing the subscribed list turns the matched marketing preference on; importing the unsubscribed list turns it off. The operations are separate and idempotent, affect no other preference or verification state, and preserve the current platform preference when a contact is absent from both lists. The supported lists are assumed clean and mutually exclusive; overlap precedence is out of scope.
MVP Slack access uses the current shared DataTalks.Club join URL. It has no Slack invitation API,
SCIM/member synchronization, or manual review queue. The join URL exists only in the approved
runtime secret channel. It is never stored in MemberProfile, SlackAccessGrant, EmailDelivery
context or retained rendered bodies, audits, logs, metrics, URLs, OpenAPI examples, screenshots, or
issue evidence. Domain rows carry only a non-secret invite_version.
For a new account, successful email verification atomically creates or confirms one access grant, one unique delivery intent keyed by account and invite version, and its durable job. After commit, the leased job resolves the current secret at send time and submits only the permitted scalar context to Relay for the verified account email. Relay validates and renders the immutable template version without returning or requiring the website to retain a secret-bearing body. Worker/Relay/ secret failure leaves account verification and Slack eligibility committed. The one-time join link is also available at the authenticated private member surface. Slack access is not a recurring preference category, is not controlled by event or marketing opt-out, and is not sent again automatically. Legacy/imported contacts bypass this verification-to-Slack sequence.
A safe Relay-owned bootstrap template may seed this purpose. Relay remains the canonical template and rendering owner throughout; the website stores only the immutable template key/version and safe scalar context needed to preserve the logical-delivery and secret-at-send/no-secret-retention contract.
An eligible member can reveal the current link at the authenticated private Slack surface. If the
secret is missing, it shows a delayed/support state rather than rolling back completion. Duplicate
submit, refresh, retry, and restart do not create another logical delivery. Invite rotation advances
invite_version; an eligible member may reveal/receive the new version without re-entering profile
data. Ordinary profile edits do not revoke access. Account quarantine, disablement, or deletion
denies future reveal/resend and cancels safe unsent work, but the site does not claim to revoke an
already used external Slack membership.
MVP has no member-facing email-resend action because the reveal page remains available. An authorized staff resend is a separate audited logical delivery with confirmation, reason, idempotency key, and rate limits. It never discloses the raw join URL through Studio/admin API.
The existing account-settings preference mechanism is the canonical preference center; no parallel toggle system is introduced. It exposes the optional event and marketing/newsletter categories and continues to expose existing course categories. Slack access, account verification, and password recovery are non-optional one-time or security messages and do not receive an optional unsubscribe toggle.
A visible unsubscribe link in a message body or footer opens the website preference page. The recipient can untick the categories they no longer want and submit; the initial GET does not mutate preference state. A supported Gmail/email-header one-click action may disable the category represented by that message immediately, idempotently, and without changing unrelated categories.
Optional marketing/newsletter delivery is sent only to verified contacts plus the explicit
legacy/imported audience, which is treated as assumed verified for newsletter delivery. A new
account must complete email verification before it enters that audience. An identifiable logged-in
interaction or a recipient-specific newsletter link may move a legacy contact from legacy to
verified; this is not a delivery gate and does not trigger another email. A bare anonymous visit
does not identify a contact and cannot change status. Newsletter attribution and preference links
use opaque, recipient- and purpose-scoped, expiring, replay-safe tokens; raw email addresses are
never placed in URLs or logs.
Relay is the canonical template catalog, validator, renderer, and sender-policy owner. It owns one editable draft, immutable published versions, subject/plain/HTML content, required typed context, safe escaping/sanitization, accessible plain/HTML parity, sender resolution, and the rendered snapshot queued for transport. Tracking pixels are disabled in MVP.
Studio and the admin API proxy the same website application services to Relay for catalog, draft, preview, publish/republish, and controlled test-send operations. Every real or test delivery names an immutable Relay template key/version. The website stores no second mutable template body, renderer, resolved sender, or retained rendered message. Relay unavailability fails safely with no local rendering or direct-send fallback.
- UUID and unique logical
idempotency_key; - classification, purpose, immutable Relay template key/version, and related account/profile/grant/event/cohort/registration/enrollment scalar IDs;
- only recipient/reference data allowed by retention policy, minimal scalar context or its canonical hash, the Relay sender ID, and safe correlation/idempotency values;
- durable-job reference plus the redacted Relay message ID, projected state, callback/reconciliation freshness, safe reason code, and timestamps.
Projected states: pending, queued, leased, provider_accepted, delivered, retryable,
ambiguous, suppressed, dead, hard_bounced, and complained. Relay is authoritative for
transport state; the website projection exists only for product behavior and operations.
The Slack-link purpose follows the strict secret-at-send contract: its durable website row stores only the fixed purpose/template identifiers and safe scalar context needed to resolve the current invite version. The website never retains any rendered body or raw provider payload; Relay's queued snapshot remains under its own reviewed retention contract. The Slack secret and rendered secret-bearing body are never returned to or retained by the website.
- Relay owns provider attempts/events, leases/backoff, suppression, and authoritative transition history. The website does not create a second provider-attempt or provider-event store.
- Relay callbacks use a tenant-scoped, timestamped HMAC over the raw body. The website deduplicates stable Relay event IDs, enforces the replay window, and applies reordered events through guarded monotonic projection transitions.
- Callback payloads and the website projection contain safe Relay/correlation/template identifiers, timestamps, state, and bounded reason codes only—not bodies, credentials, full recipient data, or raw provider payloads.
- Scheduled bounded reconciliation recovers missed callbacks and rechecks recent terminal messages; one-delivery manual reconciliation uses the same service.
- Provider acceptance is not displayed as delivered. Hard bounce, complaint, and suppression remain Relay-authoritative and independent of optional marketing preferences.
- Business state, one logical
EmailDelivery, and one durable website job are committed in the same transaction; rollback creates none. - A website worker leases/fences the durable job and calls Relay only after commit. Domain services, request transactions, model hooks, and callbacks never perform the Relay network request.
- Exact Relay submission replay uses the same idempotency key and request hash. A changed request conflicts rather than sending different work under the same key.
- Relay owns provider leases, attempts, bounded backoff, claim-time suppression, and terminal state.
- An uncertain Relay/provider acknowledgement becomes
ambiguous; neither the website job nor Relay automatically resends it. Reconciliation or an audited operator action resolves it. - Manual resend creates a new audited logical delivery related to the original; it is distinct from an automatic retry.
- Datamailer and direct Amazon SES are never fallback senders, including during rollback.
- The website calls Relay only through scoped, expiring tenant credentials and identifies senders by approved Relay sender ID. It has no provider credential, direct Amazon SES permission, sender resolution logic, or provider-event ingress.
- Relay owns provider credentials/identities, SPF/DKIM/DMARC/provider configuration, submission, queues/workers, attempts/events, suppression, quotas, and provider diagnostics.
- The only approved development sender ID is
courses, mapped by Relay toDataTalks.Club Courses <courses@dtcdev.click>. It remains recipient-allowlisted/simulated except for one separately approved controlled canary. - Unknown purposes/senders, non-course purposes still open in #22, broad recipients, and all production sender/domain configuration fail closed.
- Registration and resend endpoints use CSRF, body limits, distributed rate limits by IP/email/event, and a honeypot; adaptive accessible challenge is added only when evidence warrants it.
- Responses do not disclose whether an email is registered.
- Join links and attendee lists are not public by default.
- Privacy acknowledgement and optional marketing consent are unbundled and versioned.
- CSV exports escape spreadsheet formulas and require a dedicated permission plus audit event.
- Raw IP/user-agent data has a short documented retention and never appears in routine logs.
Published anonymous-stable event catalog/detail responses may use the 60-second public edge class with stale-if-error at most 5 minutes. Event registration forms and outcomes, verification and management links, attendance, exports, provider endpoints, Slack/profile/onboarding, and any credentialed response remain zero-TTL, private/no-store. WAF starts in count mode with per-source-IP five-minute thresholds of 2,000 for ordinary public cacheable reads and 60 for signup, login, profile, Slack, and course/event registration paths. Application services retain stricter limits by normalized identity plus safe IP class; edge rate limits are neither account authorization nor a complete distributed-bot defense.
Both interfaces can:
- create/edit/preview/publish/cancel/archive events;
- assign/reorder people as speakers/hosts;
- inspect registration counts and individual records subject to PII permissions;
- cancel/reactivate/correct a registration through explicit reasoned commands;
- export authorized registrations;
- mark attendance individually or through bounded import;
- initiate/resume event update, cancellation, reminder, or follow-up operations;
- proxy Relay template catalog/draft/version preview, test, publish, and republish operations;
- inspect the website intent plus redacted Relay status projection and invoke guarded reconcile/safe-retry/ambiguity-resolution/manual-resend commands;
- inspect redacted Relay suppression/provider-health summaries without storing provider events or exposing raw payloads.
- Registration replay, refresh, and concurrency create one logical row and at most one logical delivery per purpose/version; suppressed preferences create no Relay submission.
- Verification/cancellation tokens are hashed, scoped, expiring, revocable, redacted, and link-scanner safe.
- Event messages are sent only to active registrants whose event preference allows them; opting out suppresses verification, confirmation, cancellation, reschedule, reminder, and other event mail.
- Event reschedule/cancellation produces correct calendar sequence and idempotent messages when event mail is enabled.
- Worker and provider failures produce the specified durable states without losing business data.
- Business state, one logical delivery intent, and one durable job commit atomically; only a leased after-commit job calls Relay, and uncertainty is never automatically resent.
- Course reminders and event messages share one auditable delivery model.
- New-account email verification precedes one durable secret-free Slack-link delivery, and the authenticated member area exposes the link; legacy/imported contacts bypass this sequence. Reveal, send, retry, rotation, resend, suppression, outage, quarantine, and deletion never leak or retain the join URL, and automatic duplicate Slack sends do not occur.
- The existing preference center exposes event and marketing/newsletter categories, enforces them before Relay submission, and does not create a recurring Slack toggle. Body unsubscribe links open the category preference page; supported email-header one-click actions disable only their category.
- New account marketing preference defaults on after signup, while newsletter delivery excludes unverified new accounts. Mailchimp subscribed/unsubscribed imports independently turn the legacy marketing preference on/off; absence preserves the current platform preference, and list overlap is outside the supported input contract.
- Public event catalog/detail caching cannot store a registration, management, provider, profile, Slack, or credentialed response.
- Public Event links, canonical/OG/JSON-LD, breadcrumbs, registration/calendar builders, feeds, and
sitemaps emit only
/events/<positive-public-id>/<current-title-slug>; UUID remains management-only identity and is never a public redirect source. - Every event/email management action has Studio/admin API parity and negative authorization tests.
- New website code has no direct Amazon SES or Datamailer send path, no canonical mutable template store, and no provider-attempt/event stack; every supported purpose uses approved Relay routing, preference checks, recipient rules, and development allowlisting before it can progress.
The events app may derive reviewed historical totals from protected Luma and Eventbrite sources.
It persists source-run provenance, exact provider-event mappings, immutable per-event aggregate
revisions, coverage/combination policy, active pointers, and safe audits only. It never creates a
legacy registration row or stores a legacy name, email, guest/attendee ID or digest, answer,
consent, privacy acknowledgement, timestamp, filename/path, or provider payload. Derivation causes
no verification, transactional email, newsletter, sponsor, consent, or provider side effect.
Status policy version 1 counts Luma approved and Eventbrite Attending as registrations.
Declined, cancelled, rejected, duplicate, malformed, unknown, excluded, or quarantined records do
not contribute; unknown status quarantines its provider-event aggregate. No source status is
attendance/check-in evidence.
Each accepted contribution occupies one canonical-event/provider/coverage slot. Multiple providers
are not additive without reviewed additive_disjoint coverage. replacement atomically changes
the winning pointer; exclude contributes nothing; ambiguous overlap fails closed. Future native
confirmed/attended/no-show rows use a separately reviewed cutover slot, and a later row-level
replacement atomically supersedes the aggregate so the query can never count both. Rollback changes
pointers and revisions without deleting history. A replacement records an immutable displacement
for every prior active aggregate or row-projection pointer, so rollback restores the complete prior
accepted contribution set rather than selecting one superseded revision heuristically.
Activation preflights the complete source run before changing any pointer. Two candidates in one
run may not target the same canonical-event/provider/coverage slot; that collision fails closed
before activation, so candidates from the run cannot displace one another or obscure the exact
pre-run contribution set required by rollback.