Events & Club Ministry for the Iowa-Missouri Conference of Seventh-day Adventists
One place to find events, register a family or a whole club, run check-in, and keep Pathfinder and Adventurer club records up to date.
Public home · Club portal · Club coordination. All screenshots use fictional training data.
|
|
|
|
- Permissions on the server. Every page and API checks the event or club role before reading or writing.
- Two-step sign-in is required for administrators and for club pages that hold youth records.
- Sensitive data is sealed. Secrets and birth dates are encrypted; passwords, session tokens and private links are stored only as digests.
- Careful by default. Unsaved changes prompt before you leave a form, deletes and cancels ask for confirmation, and payments default to Square Sandbox until an explicit production unlock.
- Accessible. 44 px tap targets, visible focus rings, readable text sizes and mobile layouts are checked by tests and a browser audit. See
docs/SITE-RULES.md.
| 🎓 Training pack (November 2026) | Getting-started deck, presenter notes, quick-start handout and screenshot gallery |
| 📏 Site rules | Every approved look, behavior and sign-in rule, each with its test |
| 🎨 Design system | Colors, type, components and layout |
| 🚀 Deploy with Docker | Production setup, backups and the uptime monitor |
| 🧱 Architecture decisions | Why the system is built the way it is |
| 🗺️ Roadmap (#98) | The ordered build queue |
| 🤝 Contributor and agent guide | Repository map, checks and workflow rules |
- Node.js 20.9 or newer
- Next.js App Router, React, TypeScript, and Tailwind CSS
- PostgreSQL 16 and Prisma 6
- Zod environment and route validation
- Vitest for unit tests
- Docker Compose for the local database
-
Install dependencies and create local environment configuration:
npm install cp .env.example .env
Then set
SECRET_ENCRYPTION_KEYin.envto any 32 or more characters (for example the output ofopenssl rand -base64 32). Admin sign-in needs it to enrol two-factor authentication and fails without it. -
Start PostgreSQL:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d postgres
The dev overlay publishes the database on
localhost:5432for host access. The basedocker-compose.ymlkeeps Postgres internal-only and also builds and runs the app container for deployment — seedocs/DEPLOY-DOCKER.md. -
Apply the committed migrations, load fictitious seed data, and publish the current local demo form:
npm run db:deploy npm run db:seed npm run db:refresh-demo
-
Start the application:
npm run dev
Open http://localhost:3000. The root route is the public home page; choose Sign in to reach the staff workspace or an account.
Local seeded credentials:
Email: admin@imsda-events.test Password: IMSDA-Local-2026!The same password is seeded for the
registration@,finance@,communications@,checkin@,readonly@, andsystem@imsda-events.testrole-test accounts. All are fictitious and local-only.This password is for local work only and can never reach a real deployment.
npm run db:seedrefuses to run withNODE_ENV=productionor against any non-loopback database host, and the sign-in page shows and prefills these credentials only outside production. A real deployment bootstraps its first administrator withnpm run admin:create— seedocs/DEPLOY-DOCKER.md.The public Women’s Retreat form includes the fictitious promo code
LOCAL10afternpm run db:seedornpm run db:refresh-demo. It applies 10% off with a $50 maximum, $100 minimum subtotal, and at least 25 local test uses remaining after each demo refresh.
To stop the local database without deleting its volume, run docker compose down.
Square Sandbox checkout
Leave the Square values blank for a friendly pay-later/unconfigured experience.
To test card checkout, copy Sandbox-only values into .env for
SQUARE_APPLICATION_ID, SQUARE_ACCESS_TOKEN, SQUARE_LOCATION_ID, and
SQUARE_WEBHOOK_SIGNATURE_KEY. Configure Square's notification URL to exactly
match SQUARE_WEBHOOK_NOTIFICATION_URL, then subscribe to payment.created,
payment.updated, refund.created, and refund.updated.
The Web Payments SDK appears on a private registration management page only when the immutable submission selected card, or a promoted waitlist registrant explicitly chooses card, and a positive server-derived balance remains. Promoted registrants see card and pay-later totals before choosing; the card fee is recomputed once from the preserved discounted subtotal. Card data is entered directly in Square's hosted fields; the application stores neither card data nor Square's short-lived source token.
The same embedded Web Payments SDK checkout offers Apple Pay and Google Pay
when the buyer's browser, device, wallet, and Square account support them.
Google Pay requires HTTPS in production. Apple Pay additionally requires the
public payment-page domain to be registered in both the Sandbox and Production
views of the Square Developer Console. This application serves Square's current
domain-association file at
/.well-known/apple-developer-merchantid-domain-association; no Apple merchant
ID or extra application environment variable is required.
See modules/payments/README.md for the
idempotency, webhook, refund, and production-safety boundary.
Two-factor authentication
SYSTEM_ADMIN and EVENT_ADMIN accounts must carry a TOTP second factor.
Enforcement happens at sign-in rather than in permission checks: a correct
password yields a challenge, not a session, and an account that has not enrolled
is sent to enrol inside that challenge — so no privileged session can exist
behind a password alone, and no authorization check has to know MFA exists.
Secrets are sealed with AES-256-GCM under a key derived from
SECRET_ENCRYPTION_KEY (lib/secret-box.ts), because verifying a code needs the
secret itself and it therefore cannot be stored as a digest like everything else
in modules/access. An accepted time step is recorded, so a code cannot be
replayed inside its own thirty seconds. Ten single-use recovery codes are issued
per enrolment and stored as digests; npm run admin:reset-mfa -- --email <address>
is the operator's way back for an account that has lost both.
Other roles may enrol voluntarily from More → Two-factor authentication.
Account email
Activation, password-reset, attendee verification, and attendee edit-code email
travel the same outbox as registration messages — with backoff, attempt history,
and the scheduled sweep — but belong to a person rather than to an event, so they take their sender from
ACCOUNT_EMAIL_SENDER_ADDRESS (plus ACCOUNT_EMAIL_SENDER_NAME and the
optional ACCOUNT_EMAIL_REPLY_TO) instead of an event's message settings.
Production requires that address and RESEND_API_KEY: a colleague who never
receives an invitation cannot obtain a credential at all.
Locally, leaving them blank keeps the previous behaviour — Forgot password shows the one-time link on the page, and an invitation returns it to the administrator — and the page says which of the two is happening.
The queued message stores a sentinel, never a token or code. The credential is minted when the message is actually sent, so its short lifetime starts on delivery rather than in the queue. A retry reuses the same encrypted credential instead of invalidating the first message while the recipient is typing it.
Attendee accounts
Attendee identity stays separate from staff identity. A verified attendee account claims registrations only by the normalized registration contact email; it cannot hold an event permission. The account page supports registrations across events, a reusable profile that prefills matching fields, optional TOTP, Square balance payment, and contact edits protected by a single-use emailed code bound to the session that requested it.
Staff can explicitly switch from the staff shell into a real attendee session for their own matching account when the normalized staff email matches an active verified attendee account. The switch endpoint accepts no attendee ID, so it cannot select or impersonate another attendee. The staff session remains available through Staff workspace (and System management for system administrators) in the My account header, next to Sign out, which ends both sessions. Staff without a matching account retain the read-only event preview.
Google identities use the provider subject as the durable key. A verified
same-email Google-only account may safely rebind after Google changes that
subject; mixed password-and-Google accounts require explicit support rather than
an automatic rebind. Event edit policy is stored per event, inherited from the
platform default, with VERIFY_EVERY_EDIT as the default and the Women's Retreat
kept on TIERED.
See the attendee-account release checklist before enabling these paths against production providers.
Registration embeds
Event settings generates one complete iframe-and-script block for each published form. The embed-only layout reports its content height as fields, steps, validation, and confirmation change; the host helper validates the message origin, resizes the exact sending iframe, and scrolls that frame back into view when the registrant advances or needs to correct an error. The 900px iframe height remains a usable fallback when host JavaScript is blocked.
EMBED_ALLOWED_ORIGINS is the authoritative frame-ancestors allow-list. The
embed flow is anonymous and stateless: registration submission does not depend
on a third-party session or CSRF cookie, and payment continues from the private
top-level registration page.
Attendee community
Each event may opt in to a community for attendee accounts with an active
registration. An attendee accepts the event’s current conduct version before
posting, can create a post or one-level reply, choose NONE, REPLIES, or
ALL in-app notifications, and report content for staff review.
Communications managers can pause posts/replies, edit conduct copy, set post-event retention, hide/restore/remove content, and resolve reports. Staff preview is intentionally read-only; it cannot create an attendee-authored action. The scheduled outbox sweep also performs event-end retention cleanup. See ADR 0004.
npm run lint
npm run typecheck
npm run test
npm run build
npm run verifynpm run verify runs lint, generated route types plus TypeScript, unit tests, and the production build. CI additionally starts PostgreSQL, applies the migration, and seeds a clean database before verification.
With PostgreSQL and the app running (npm run dev, or a build started with npm run start and its production settings), the live concurrency/integration checks are:
npm run test:public-capacity
npm run test:public-access
npm run test:public-messaging
npm run test:public-rosterEach one creates its own throwaway event with dates relative to today, so none depends on a seeded event's deadlines. CI runs all four against the built app, plus npm run test:eadventist-import, which migrates and drops its own scratch database and so never touches your development data. The real-browser name-badge check, npm run test:badge-print, runs in its own optional CI job; see docs/BADGE-PRINT-CHECK.md.
| Command | Purpose |
|---|---|
npm run db:generate |
Regenerate the Prisma client. |
npm run db:migrate -- --name <name> |
Create and apply a development migration. |
npm run db:deploy |
Apply committed migrations without creating new ones. |
npm run db:seed |
Upsert fictitious .test foundation data. |
npm run db:refresh-demo |
Publish the current Women’s Retreat template only in the seeded localhost database, through the same save, test and publish workflow staff use. Replaces an out-of-date local draft with the template, and is safe to run repeatedly: when the published form already matches it, the form is left as is (the LOCAL10 promo code is re-upserted each time). |
npm run db:studio |
Open Prisma Studio against the configured database. |
Never place real attendee exports, medical details, production identifiers, or credentials in migrations, seeds, tests, issues, screenshots, or commits.
Show the current API (route reference)
-
POST /api/auth/loginvalidates a staff credential and creates an eight-hour database session. -
POST /api/auth/logoutrevokes the current session and expires its HttpOnly cookie. -
POST /api/auth/password-reset/requestqueues a single-use 30-minute reset credential for account email delivery; development can return a local link when delivery is unconfigured. -
POST /api/auth/password-reset/completeupdates the scrypt password hash and revokes all prior sessions. -
GET /api/healthanswersstatus,checkedAtand the database check to anyone. With theOUTBOX_SWEEP_TOKENorHEALTH_MONITOR_TOKENbearer token, or a system-admin session, it also shows the message outbox, sweep, backups, encryption key source and the running release SHA/build ID (seedocs/DEPLOY-DOCKER.md, "Uptime monitor"). -
GET /api/eventsreturns only events assigned to the development session user. -
GET /api/events/:eventId/overviewvalidates the event ID, verifies active event membership andVIEW_EVENT, then returns database-derived totals. -
GET|POST /api/events/:eventId/registrationslists or creates registrations after event-scoped permission checks. -
GET|PATCH /api/events/:eventId/registrations/:registrationIdloads or edits a registration and its primary attendee. -
POST /api/events/:eventId/registrations/:registrationId/lifecycle/:actionperforms an explicit cancel, reactivate, waitlist, or promote transition with capacity release/restore, audit history, and transactional notices. -
POST /api/events/:eventId/registrations/:registrationId/transferatomically changes the registration owner/contact while preserving all registration facts, revokes prior private access, and queues notices. -
POST /api/events/:eventId/registrations/:registrationId/attendees/:attendeeId/substitutionreplaces one unchecked-in attendee in place while preserving the attendee row, answers, pricing, and capacity claims. -
POST /api/events/:eventId/registrations/:registrationId/attendeesadds a person to a registration party. -
POST /api/events/:eventId/registrations/:registrationId/paymentsrecords a validated cash, check, or other manual payment. -
GET|POST /api/events/:eventId/promo-codeslists or creates event discounts for finance managers; the API never returns attendee data. -
PATCH /api/events/:eventId/promo-codes/:promoCodeIdedits future uses or deactivates a code with optimistic conflict protection. -
POST /api/events/:eventId/payments/:paymentId/refundsrecords a full or partial refund against an offline payment; Square card refunds must be initiated in Square. -
GET|POST /api/public/manage/:token/paymentreturns a private server balance quote or submits an idempotent Square payment using a short-lived browser token. -
POST /api/public/manage/:token/payment-choiceidempotently saves card or pay-later for a promoted waitlist registration, with optimistic conflict protection and a server-owned fee quote. -
POST /api/webhooks/squareverifies the raw-body Square signature and idempotently reconciles payment and refund lifecycle events. -
POST /api/events/:eventId/attendee-passes/resolveverifies a signed, PII-free QR pass or resolves a manual registration confirmation code after event-scoped check-in authorization. -
GET /api/public/manage/:token/attendee-passes/:attendeeId/qrrenders a private, no-store attendee QR pass only after registration-link authorization. -
POST /api/events/:eventId/attendees/:attendeeId/check-inrecords or safely retries an arrival using a strict client UUID idempotency key;DELETEexplicitly undoes the active arrival while online. -
GET|POST /api/events/:eventId/announcementslists announcements or creates a draft. -
PATCH /api/events/:eventId/announcements/:announcementIdpublishes a draft; eligible all-attendee notices appear on the public event landing feed after their publication time. -
PATCH /api/events/:eventId/messaging-settingsupdates local-capture mode, sender/reply-to details, and internal notification recipients. -
PUT /api/events/:eventId/message-templates/:templateIdvalidates and publishes a new immutable template version. -
POST /api/events/:eventId/message-templates/:templateId/testrenders and captures a fictitious test message locally. -
POST /api/events/:eventId/messages/process-localclaims pending outbox rows and records local capture attempts. -
POST /api/events/:eventId/messages/processclaims committed outbox rows for local capture or the configured external email provider with retry/backoff. -
POST /api/events/:eventId/messages/:messageId/retryrequires a stable client UUID and server-issued request fingerprint, then idempotently creates and post-commit processes one audited immutable copy. -
GET|POST /api/events/:eventId/balance-reminderspreviews the exact current balance audience or idempotently creates the reviewed batch without silently sending external email. -
POST /api/events/:eventId/messages/:messageId/resend-confirmationcreates an audited immutable confirmation copy, optionally to one corrected destination without changing the contact record. -
GET /api/events/:eventId/exports/registrationsdownloads a formula-safe CSV roster and balance report. -
GET|POST /api/events/:eventId/membershipslists or creates staff assignments for event administrators. -
PATCH /api/events/:eventId/memberships/:membershipIdchanges a staff role or active status while preserving at least one active event administrator. -
GET /api/events/:eventId/importslists recent staging runs for authorized event administrators. -
POST /api/events/:eventId/imports/previewvalidates a CSV, stores an immutable source snapshot per row, and prepares CREATE, UPDATE, SKIP, or ERROR decisions without changing registrations. -
GET /api/events/:eventId/imports/:importRunIdreturns the reviewable records and field-level differences for one run. -
POST /api/events/:eventId/imports/:importRunId/commitapplies a reviewed, error-free run to the local database exactly once. -
GET /api/events/:eventId/imports/:importRunId/exceptionsdownloads formula-safe warnings and errors as CSV. -
GET /api/events/:eventId/imports/reconciliationreturns cumulative created, updated, skipped, and error totals. -
GET|POST /api/events/:eventId/formslists event forms and templates or creates a local draft from a template. -
GET|PATCH /api/events/:eventId/forms/:formIdreturns one versioned form or saves its draft with optimistic conflict protection. -
POST /api/events/:eventId/forms/:formId/test-submissionsvalidates and stores a fictitious response against an exact form version. -
POST /api/events/:eventId/forms/:formId/publishpublishes a draft as an immutable local version and archives the prior published version. -
GET|POST /api/events/:eventId/program-assignmentspreviews a deterministic ranked-interest assignment or applies the exact reviewed fingerprint as a new immutable, audited run. -
GET /api/events/:eventId/program-assignments/:runId/rosterdownloads the event-authorized, formula-safe CSV for one frozen assignment run. -
GET|PATCH /api/events/:eventId/communityreturns the staff moderation workspace or updates settings, post state, and reports for communications managers. -
GET|POST /api/attendee/events/:eventId/communityserves conduct, posts, replies, reports, notification preferences, and read state only to an actual attendee session with an active registration. -
POST /api/public/events/:eventSlug/forms/:formSlug/registrationsvalidates, prices, capacity-checks, waitlists if necessary, and saves an anonymous registration idempotently. -
POST /api/public/events/:eventSlug/forms/:formSlug/promo-codereturns a same-origin, rate-limited, server-calculated display quote without consuming a use. -
GET|PATCH /api/public/manage/:tokenresolves a hash-only, expiring private link and permits contact-only self-service changes. -
POST /api/webhooks/resendverifies provider events and reconciles delivery status idempotently. -
POST /api/webhooks/sesreceives Amazon SNS reports for SES: verifies the SNS signature, the allowed topic and account, then records deliveries, bounces and complaints idempotently by SNS message id and suppresses hard-bounced or complained addresses (#863).
The browser receives only a random opaque session token in an HttpOnly, SameSite cookie. The database stores its SHA-256 digest, expiry, and revocation state. Staff passwords use salted scrypt hashes; reset tokens are also stored only as digests. Mutation routes require a same-origin request in addition to authentication and event authorization.
app/ pages, layouts, and route handlers
components/ reusable shell and brand components
modules/access/ credentials, sessions, recovery, memberships, roles, and capability rules
modules/events/ event reads, selection, and overview data
modules/people/ permanent identity and household boundary
modules/registrations registration and attendee boundary
modules/payments/ payment and refund boundary
modules/promo-codes/ bounded discount, quote, redemption, and immutable snapshot boundary
modules/checkin/ check-in and reconciliation boundary
modules/communications announcements, versioned templates, transactional outbox, and delivery-history boundary
modules/community/ attendee posts, conduct, reports, moderation, notifications, and retention
modules/forms/ registration templates, definitions, tests, and immutable versions
modules/reporting/ safe operational reports, grouped retreat packets, and printable projections
modules/audit/ transaction-compatible audit interface
integrations/ future external adapters only
prisma/ schema, migration, and fictitious seed
scripts/imports/ staging CSV contract and operator notes
tests/ permission and domain tests
docs/decisions/ architecture decision records
The platform begins as a modular monolith. People and households persist across events; registrations, attendees, payments, refunds, check-ins, announcements, audits, and imports are event-scoped. PostgreSQL becomes authoritative only after staging imports, reconciliation, and cutover approval.
See ADR 0001 for the decision and consequences.
The product name, descriptor and symbol are defined once in lib/brand.ts (IMAdventist, Events & Club Ministry, public/brand/imadventist-symbol.png); see the branding inventory. The workspace preserves the calm operational structure of WR26 App V2 with the conference palette:
- IMSDA navy
#003B5C - IMSDA purple
#582C83 - IMSDA gold
#F0B323 - Noto Sans typography
- desktop sidebar and mobile bottom navigation
- event selector plus Overview, People, Check-in, Communications, and More routes
- working event selection that follows staff between pages
The current UI writes authoritative operational state to the configured IMAdventist PostgreSQL database. Staff sign-in/out, local recovery, event role/status management, registration create/edit, reviewed whole-registration transfer, in-place attendee substitution, party attendees, manual payments, partial offline refunds, signed attendee passes, camera/manual pass resolution, partial-party check-in/online undo, recoverable device-local offline check-in, CSV export, audit history, announcement draft/publish with a filtered public all-attendee feed, reviewed CSV imports, registration-form draft/test/publish, and message-template/outbox workflows are functional. A published form is available at /register/{event-slug}/{form-slug} and through an allow-listed, automatically resizing /embed/{event-slug}/{form-slug} iframe. It can save either one attendee or an ordered household/group roster locally. It records attendee-scoped answers, pricing, capacity claims, and the exact confirmation after commit. A private management link can collect the remaining card balance through Square Sandbox when configured; no real payment is possible with the default configuration.
Show the registration builder workflow
- Sign in as an event administrator or registration manager and open Registration form.
- Create a form from Simple RSVP, Retreat registration, Household interest, Women’s Retreat 2026, Man Camp 2026, Spring Camporee 2026, or Camp Meeting 2026, or edit a saved event draft.
- Add, remove, and reorder sections and fields; insert reusable modules; configure labels, stable field keys, field types, scope, choices, help text, and required status.
- Configure conditional visibility, ordinary versus capacity/ranked-interest choices, per-choice limits, prices, automatic fees, and optional date-driven late prices. Add the Promo code drop-in module when the event offers finance-managed discounts. Use the calculation-preview date to test both sides of a pricing deadline.
- Save the draft, complete the live preview with fictitious responses, and run a test submission. Validation feedback is attached to the exact saved version.
- Publish a form for the first time only after a valid test passes; later drafts publish without a retest (a test stays optional). The published definition becomes immutable, its public registration link activates, and a later edit automatically creates the next draft version while the prior public version remains live.
The builder supports short and long text, email, phone, dropdown, radio, multiselect, ranked choice, checkbox, date, number, and automatic-fee fields. Conditional logic, scheduled late pricing, card-fee gross-up, bounded promo-code rules, repeatable attendee modules, public rendering, server-side validation and pricing, immutable answer snapshots, duplicate-submit idempotency, serializable capacity reservations, automatic waitlists, private links, external email delivery, and Square payment collection are implemented. File uploads remain separate work.
Show the staging import workflow
- Sign in as an event administrator and open Imports.
- Download the template or sample CSV, or choose a compatible
.csvfile up to 2 MB and 2,000 data rows. - Select Preview import. Review row validation, identity matches, proposed actions, warnings, and field-level differences. Preview never changes a registration.
- Correct and re-upload files with errors. Warning-only runs remain eligible for a reviewed commit.
- Select Commit to local database, acknowledge the confirmation, and commit once. Repeating the same upload returns the existing run; repeating commit is safe and does not duplicate data.
- Use reconciliation totals and the exception CSV to document the result.
The format template is available at /fixtures/wr26-import-template.csv; a wholly fictitious walkthrough file is available at /fixtures/wr26-import-sample.csv. See scripts/imports/README.md for the exact field contract and matching rules.
The detailed built-versus-remaining matrix and the behavior comparison with DurantTL/WR26-IMSDA are in the build status and WR26 gap audit.
Before this workspace holds real attendee, medical, or payment data, read the production readiness review. It reviews the build plan and the shipped features against what a real deployment requires, and sequences the remaining work into ten items.
The production readiness progress log tracks that sequence: what has been implemented, what is deliberately deferred, and why. The review's three blocking findings — no path to a real administrator account, a single bad environment variable silently 403ing every write, and no backups — are addressed. MFA is complete; medical-data encryption remains a platform backlog item outside the Women’s Retreat release scope, and the deployment/Square Sandbox rehearsal remains in progress.
Signed attendee QR passes, camera/manual staff resolution, the recoverable offline check-in conflict queue, reviewed balance reminders, corrected-address confirmation copies, bounded promo-code administration, whole-registration transfer, in-place attendee substitution, printable pass sheets, grouped retreat packets, and the moderated attendee community are complete. Scheduled/targeted announcements remain later work. Production Square activation remains gated behind approved credentials, an exact public webhook URL, operational refund testing, and the explicit production unlock.
IMAdventist is built for the Iowa-Missouri Conference of Seventh-day Adventists. Questions: support@imsda.org


