Release Notes — Jun 19, 2026 — dev → prod promotion (Waitlist + Platform Check-in + hardening)
What this release is. This is the dev → prod promotion candidate (draft MR membervu/membervu-saas!200) — everything accumulated on dev since the last prod release, going out together. 142 commits · 165 files · 80 issues. Verify it on staging (https://stg-rcme.membervu.com; admin portal https://stg-admin.membervu.com), which builds from dev — staging is your stand-in for what prod will become. Grouped by area with a "What to verify" hint. Two items are new features (Waitlist, Platform event check-in) and two are new auth/security surfaces (refresh-token rotation, broadcast XSS escaping) — those carry the highest risk and come first.
🧭 New to MemberVu? Start here
MemberVu is a multi-tenant membership platform. There are two separate web apps — know which one you're in:
Org portal (stg-rcme.membervu.com) — what a club's admins and members use. Tenant = rcme (Rotary Club of Manila Expats).
Super-admin / platform portal (stg-admin.membervu.com, separate login) — what Zeniark platform operators use to manage all tenants. The new Platform Check-in feature lives here.
Recommended path for this release: (1) read the env + login box below; (2) test refresh-token sessions (§🔑) — highest-risk new auth surface; (3) test Platform Check-in (§🛰️) — new cross-tenant surface; (4) test Waitlist (§🎟️) on a full event; (5) then work the remaining sections top-to-bottom. Full module-by-module "what to re-review" map: Platform section and the campaign note (see footer).
⚙️ Before you start — environment & logins
Surface
URL
Login to use
Org portal (tenant)
stg-rcme.membervu.com
Admin admin@rcme.membervu.com / Admin123!; Member testmember@rcme.membervu.com / Member123!
Super-admin portal
stg-admin.membervu.com
Super Admin superadmin@zeniark.com / SuperAdmin123!
Email inbox (MailHog)
stg-webmail.membervu.com
—
Backend API
stg-api.membervu.com
—
Login is rate-limited (~5–30 attempts / 15 min per IP). A 429 means you're throttled — wait or ask for a backend restart.
Registration is rate-limited (5 / 15 min per IP) — space out repeated sign-up tests.
Session change this release: sessions used to expire after 15 minutes; they now stay alive via a refresh token (see §🔑). If you're testing the old 15-min logout behavior, it's intentionally gone.
Scope notes — read before testing.
Waitlist is now ON by default for every event that reaches capacity. It is computed live from capacity (there's no per-event on/off toggle, no data migration) — so existing full events will start showing waitlist behavior. It only switches off globally if the backend env var WAITLIST_ENABLED is set to 'false'.
Payment gateway is still inert — no live GCash/Maya/card/Split-Pay or webhooks. Use SimulatePayment / manual payment recording. Webhook tenant-boundary work is therefore not live-testable.
Cross-tenant security feature: Platform Check-in lets a super-admin act on a tenant's data. The reviewer gate is to confirm isolation + audit (see §🛰️ and §Security).
4 additive DB migrations ship with this release — all non-destructive. One (membership_type_is_default) needs a prod pre-flight check before deploy (see §Deploy).
Before you test this — what it is. Until now a login lasted ~15 minutes, then you were logged out. This release adds a refresh token: the app silently gets a new short-lived access token in the background, so your session keeps working past 15 minutes. Each refresh rotates the token (issues a new one, kills the old one), and logging out / resetting your password revokes the token. This affects every logged-in user in both portals.
#
Change
What to verify on stage
#359
Refresh-token rotation — new RefreshToken table (hashed tokens), rotation on use, revocation on logout / password-reset / role-change. Sessions survive past the 15-min access-token TTL.
Session survives: log in (org portal), leave the tab idle > 15 min, then click around — you should stay logged in (no surprise logout). Logout revokes: log in on two tabs, log out on one → the other tab should lose access on its next action. Password reset revokes: reset your password → existing sessions are kicked. Repeat the "stays logged in" check in the admin portal too.
Security reviewer note. The replay/reuse protection is implemented (re-using an already-rotated token is rejected) and is covered by backend tests. Your gate is to confirm it behaves end-to-end: (a) the refresh cookie is HttpOnly + Secure; (b) a logout/reset actually invalidates a still-held refresh token; (c) sessions surviving past 15 min is the intended prod posture (prod previously hard-expired at 15 min).
Before you test this — what it is & where. Platform operators (super-admins) can now open any tenant's event and manage attendance without logging into that tenant's org portal. This is a deliberate tenant-boundary crossing, so the testing emphasis is isolation + audit. Where:stg-admin.membervu.com (NOT the org portal). Login:superadmin@zeniark.com / SuperAdmin123!. Pre-req: at least one tenant event with registered attendees. This is brand-new — no prior test guide covers it; see the new Platform section.
Open a tenant → Events tab → pick an event → the Attendees page lists that event's registrants (attendance status, type). Confirm you see attendance data — not billing/financial fields (those must be redacted for operators).
#374
Platform-scoped read routes for events + attendance.
The attendee list loads for the selected tenant only. Switch to a different tenant's event → its list shows its own attendees, never the first tenant's.
#375 / #378
Check-in write actions — mark / undo / bulk — each writing an operator audit row.
Mark an attendee checked-in → state updates; undo → reverts; select several → bulk check-in. After each, confirm an audit entry records who (operator) did what. A denied/forbidden action should return a 403, not log you out.
#388
QR scan-to-check-in on the Attendees page.
Use the QR scanner → scanning a registrant's ticket QR checks them in. (No camera? Confirm the scanner UI opens without error — #405 removed a broken prop.)
#397
NO_SHOW status + reconcile core (DB migration adds the enum value).
Mark a confirmed attendee as NO_SHOW → status shows NO_SHOW; it's excluded from checked-in counts; it appears correctly in attendance reports/exports.
Upload an attendance CSV → preview matches rows to registrants (invalid/duplicate flagged) → commit. Finalize marks remaining confirmed-but-unscanned attendees as NO_SHOW. Verify the same reconcile flow on the org portal too (#399).
Isolation gate (do this with two tenants). Check in an attendee for tenant A. Confirm via the tenant A org portal (or DB) that only tenant A's record changed and tenant B's identical event is untouched. UI looking right is not enough — isolation lives in the backend tenantId filter.
🎟️ Waitlist — now ON by default (NEW feature) — #301–#308, #333–#335
Before you test this — behavior change. When an event is full, new registrations no longer fail — they go onto a waitlist. An admin can then approve (optionally overriding capacity) or decline waitlisted people. This is ON for all events by default. Pre-req: create or pick an event and fill it to capacity so the next registration hits the waitlist.
#
Change
What to verify on stage
#301 / #302
Registration diverts to waitlist when full — member, member+guest, and public/anonymous paths.
Fill an event. Then register as a member, as a member adding a guest, and as an anonymous public visitor → each should be placed on the waitlist (not rejected, not given a seat).
#305
Admin waitlist queue — read endpoint + UI.
As admin, open the event → the waitlist queue shows the people who got diverted, in order.
Approve a waitlisted person → they get a confirmed seat (capacity override works even when full). Decline another → removed from queue. Approving someone already CHECKED_IN returns a 409 (guard).
#308
Approved-guest claim link — approval email mints a valid claim token.
Approve a brand-new guest from the waitlist → they get an email; the claim link works (resolves, lets them set up / confirm) and its host is the correct tenant origin (not a broken/wrong domain). Check the email in stg-webmail.membervu.com.
#333 / #334
waitlistActive in the public event DTO + "Join Waitlist" CTA on the anonymous public event page.
On a full event's public page (logged out), the CTA reads "Join Waitlist" instead of "Register".
#335
Waitlisted guests do not receive the join-time setup email.
A guest placed on the waitlist should not get the account-setup email at join time (only on approval). Check MailHog.
Event edit round-trip — maxGuestsPerMember persists (#280, was the open dropped-payload bug); endsAt can now be cleared (nullable, DB migration) (#328).
Edit an event → set the per-member guest limit and an end time → save → refresh → both persist. Then clear the end time → save → refresh → it stays empty (no revert).
#311
Guest ticket / QR portal links use the correct tenant origin.
A guest's ticket/QR link points at the tenant's own domain (not a wrong/broken host).
#319
RSVP / invoice / waitlist emails render the tenant-local date.
Event emails show dates in the tenant's timezone, not UTC/server time. Check MailHog.
#320–#322, #325
Avatar save-on-change + SVG rejection + remove-photo (#320/#325); API validation errors surfaced on the registration form (#321); stray leading dash removed from phone display (#322).
Registration form shows real validation errors; phone numbers don't show a leading dash; avatar upload rejects SVG and supports remove.
#326–#331
Event-management batch — incl. a PUT-crash fix on event update.
Editing and saving an event no longer 500s; the management screens behave.
#347
Configured ticket types display on event detail.
An event with ticket types shows them on its detail page.
From a member's Account page, submit bad input (malformed email/phone, oversized field) → rejected with a clear error; valid edits save. Re-test on an existing member record — hardening can reject values that used to slip through.
Merge-tag personalization + preview guard, with HTML-escaping of merge-tag values (XSS hardening). Send a broadcast where a merge value contains <script>/HTML → confirm it's escaped in the email, while a normal {{first_name}} still substitutes correctly (not double-escaped).
#228
exports
Native .xlsx output on existing export endpoints. Export → file opens as a real Excel sheet with correct headers/data; the CSV-injection guard (#336/#340) still applies.
#348
storage/images
Store bare S3 key for avatars + presign images inline. Upload an avatar → it renders for anonymous viewers after refresh (not a broken/download link).
#372
rbac
Role seeding is now additive + startup backfill (no overwrite). Existing custom role/permission state is preserved on deploy.
#405
admin-portal
Removed an invalid audio prop from the QR scanner (follow-up to #388) — scanner opens cleanly.
🖼️ Media / logo storage hardening — #406, #407, #410, #411, #409 (code-only)
#
Change
What to verify on stage
#406 / #407
Store bare S3 key for tenant/invoice logos; canonicalize logoUrl to bare key on org-profile write (no backfill).
Upload a tenant logo + invoice logo → refresh → renders correctly. Re-save the org profile → the stored value stays a bare key (a presigned URL isn't re-persisted). Verify existing (old full-URL) logos still render after deploy.
#410
Guard external http(s):// avatar URLs from the redirect-service rewrite (regression fix on #348).
A member whose avatar is an external URL → it renders untouched (not mangled by the image redirect).
CSV / formula-injection neutralized across all exports.
Export data containing a cell starting with =, +, -, or @ → the export neutralizes it (e.g. prefixed) so it won't execute as a formula in Excel. Confirm normal numbers/text are not mangled.
#340
Guest CSV export moved server-side + FE injection guard.
The guest export downloads from the server (not assembled in the browser) and is sanitized.
#252
Broadcast merge-tag values HTML-escaped (see §Exports above).
Regression sweep: bug batch 2 (#361–#369), mobile (#351/#367), FE polish.
Skip: live gateway/webhook flows (inert).
🚀 Deploy / migration notes (ops + reviewer gate)
Four additive migrations — deploy via prisma migrate deploy only; never reset/reseed on prod.
make_event_endsat_nullable (#328) — drops NOT NULL on Event.endsAt.
add_membership_type_is_default (#365) — adds column + partial unique index. ⚠️ Pre-flight required: if prod already has >1 default per tenant, the index creation fails. Run SELECT "tenantId", COUNT(*) FROM "MembershipType" WHERE "isDefault"=true GROUP BY "tenantId" HAVING COUNT(*)>1; — must return zero rows.
add_no_show_status (#397) — adds NO_SHOW enum value. Irreversible in PG (enum values can't be dropped).
add_refresh_token (#359) — new RefreshToken table; reversible by DROP TABLE. ⚠️ auth-service/package.json changed — the prod image rebuild must pick up the new dependency.
Manual script (#363) — do NOT run on prod without review:scripts/sync-bank-accounts-permission.ts is not auto-run and is not required (the permission also lands via the additive #372 backfill). It deletes+recreates system-role permission joins — only safe while no per-tenant system-role customisation exists in prod.
Source of truth: the membervu/membervu-pm tracker + draft MR membervu/membervu-saas!200. This page is the tester/reviewer-facing view; the full campaign note (focused test plan, deploy gate, module re-review matrix) is tester-files/03_EXECUTION_REPORTS/prod-release-2026-06-19/RELEASE_NOTES.md; the permanent archive entry is tester-files/07_RELEASE_NOTES/2026-06-19-release-notes.md. Verified present on dev on 2026-06-24. Gateway flows inert in all QA environments.