revento

revento documentation

Introduction

revento is a multi-tenant event RSVP, ticketing, and onsite redemption platform. Organizations run invite-only events end to end: tenants and admins manage campaigns from an internal dashboard, invitees RSVP through a public landing page and receive a QR ticket, and onsite officers validate and redeem those tickets from a mobile PWA — with email / WhatsApp notifications and a full activity log behind it all.

What revento does

CapabilityDescription
Multi-tenancyEach tenant owns its campaigns, invitees, officers, and notification channels.
Campaign managementA guided wizard creates campaigns: event window, venue + map, RSVP window, redemption window, landing assets, and notification templates.
Invitee + referralInvitees are added individually or via CSV bulk upload (Name, Email, WhatsApp, Remarks, optional Referral Code); each gets a unique referral code that gates RSVP. Duplicate email, WhatsApp, or referral rows are skipped during import.
Public RSVPInvitees RSVP on a per-campaign landing page; a valid referral mints a booking code + QR ticket.
Onsite redemptionOfficers log in with a PIN on a mobile PWA, validate a booking code / QR, and redeem the ticket. Duplicate and invalid attempts are blocked and logged.
NotificationsTransactional email (Resend) and WhatsApp (WAHA) on invitation, RSVP response, and redeem-complete.
Campaign reportOne-click official report exported as a server-rendered PDF, with an optional AI-generated insight.
RBACFour admin roles plus officers, with tenant- and campaign-scoped access.
AuditingActivity logs, notification logs, and scan-attempt logs across every flow.

This documentation describes the platform as of the current release. The full API contract is in API Reference; the canonical changelog and versioning policy are under Releases & versioning.

Quick start

Run revento locally against a Supabase project.

Prerequisites

  • Node.js 20+
  • A Supabase project (Postgres)
  • npm (a package-lock.json is committed)

Setup

# 1. Install dependencies
npm install

# 2. Configure environment — create .env.local (see Environment variables)

# 3. Apply database migrations to your Supabase project
#    (Supabase CLI or SQL editor — see supabase/migrations/)

# 4. Run the dev server
npm run dev

Open http://localhost:3000.

Demo accounts

The demo seed provisions a sample tenant + campaign and an account for every role.

SurfaceWhereCredentials
Dashboard (super admin)/loginsuper@revento.id / Qwerty123!
Dashboard (admin)/loginadmin@revento.id / Qwerty123!
Dashboard (tenant)/loginadmin-tenant@revento.id / Qwerty123!
Dashboard (tenant ops)/loginadmin-tenant-ops@revento.id / Qwerty123!
Public RSVP/IIMX2026referral code DEMO01
Officer PWA/officer/IIMX2026PIN 123456

Demo credentials are for local development only — never seed them into production.

Architecture

revento is a single Next.js 16 (App Router) application on React 19, TypeScript, and Tailwind CSS v4, backed by Supabase (Postgres). Safety-critical domain logic is written with Effect as typed, tagged-error pipelines.

How requests flow

  • The Dashboard uses React Server Components + Server Actions for all CRUD. src/proxy.ts (Next 16 middleware) verifies the JWT session cookie before any /admin/* request reaches a page.
  • Public RSVP and the Officer PWA call Route Handlers under /api/*; the dashboard's report export hits /api/admin/:slug/report.
  • Core business logic lives in a service layer. RSVP submission and officer redemption are Effect pipelines that map cleanly to HTTP status codes; plain async services handle PDF report assembly and AI insight.
  • The most safety-critical write (RSVP → ticket) is a single Postgres RPC (submit_rsvp) that locks the invitee row, is idempotent on re-submit, and creates the RSVP + ticket + activity log atomically.

Tech stack

AreaChoice
FrameworkNext.js 16.2.6 (App Router, RSC, Server Actions)
UI runtimeReact 19.2.4
StylingTailwind CSS v4, next-themes
Dashboard UISmoothUI + Radix Slot, CVA, clsx, tailwind-merge
Animationmotion · lenis (marketing smooth scroll)
DatabaseSupabase (Postgres) via @supabase/supabase-js
Domain logiceffect (typed errors + transactional pipelines)
Authjose (JWT HS256), bcryptjs (password + officer PIN hashing)
Emailresend
PDF reports@react-pdf/renderer (server-rendered)
AI insightAny OpenAI-compatible Chat Completions API (DeepSeek default)
TestingPlaywright

The four surfaces

revento is one app with deliberately separate front-end surfaces. They do not share a shell — this separation is a hard rule in the codebase.

SurfaceAudienceRoute prefixStyling
Internal DashboardSuper admins, admins, tenant ops/admin/*SmoothUI + Tailwind tokens, dark default, JWT-guarded
Public RSVPInvitees/{slug}Custom landing CSS + motion, always light
Officer PWAOnsite officers/officer/{slug}100% inline styles, mobile-first
Marketing siteVisitors/Scoped dark cinematic theme + Lenis

This documentation surface (/documentation) is a fifth, self-contained surface — it does not inherit the dashboard shell and is linked only from the dashboard footer.

Data model

Core entities live in Supabase Postgres — 15 tables. DB rows (snake_case) are mapped to TS shapes (camelCase) in src/lib/db/mappers.ts; domain types live in src/lib/types/domain.ts.

Key relationships

  • tenants own campaigns and configure notification_channel_options.
  • campaigns have invitees and are staffed by officers.
  • An invitee submits one rsvp, which mints one ticket.
  • A ticket is redeemed via redemptions performed by officers.
  • Every flow writes to activity_logs, notification_logs, or scan_attempt_logs.

Full table list

tenants, campaigns, invitees, rsvps, tickets, officers, officer_sessions, redemptions, scan_attempt_logs, admins, magic_link_tokens, notification_channel_options, notification_logs, activity_logs, app_settings.

RLS is deny-by-default; the service_role is granted for server-side access only. Field-level schemas for the key entities are in Data schemas under API Reference.

Roles & RBAC

canAdminAccessCampaign(admin, campaign) (src/lib/auth/rbac.ts) enforces scope.

RoleCampaign access
super_adminEverything
adminEverything
admin_tenantCampaigns whose tenantId is in assignedTenantIds (empty campaign list = all current + future campaigns of the tenant)
admin_tenant_opsTenant match AND campaign in assignedCampaignIds
officerNo dashboard; PIN-scoped to one campaign's redemption

The /admin/rbac page renders a role × menu matrix over the menu keys dashboard, tenant, campaign, admin, rbac, setting.

State machines

Campaign status

scheduled → active → paused → active → closed → archived

A campaign moves from scheduled to active, can be paused and resumed, then closed and finally archived.

Ticket status

active ─┬→ redeemed   (officer redeems)
        ├→ revoked    (admin revokes)
        └→ expired    (window passes)

Validation and redemption refuse paused / closed / archived campaigns and any request outside the redemption window.

Campaigns

Campaigns are created from /admin/campaigns/new with a guided wizard. Each campaign owns its slug, event window, venue + map, RSVP window, redemption window, landing assets, and notification templates.

Wizard steps

  1. Basics — name, unique slug, tenant, event date window.
  2. Venue — address and map coordinates.
  3. Windows — RSVP open/close and redemption open/close.
  4. Notifications — rich-text email subject + body templates (alignment, font size/weight) with parameter-tag chips.
  5. Landing — landing theme, logos, and assets (uploaded via the assets API).

Reserved slugs (admin, officer, api, login, documentation, …) live in src/lib/constants/routes.ts so a campaign slug can never collide with a system route.

RSVP & ticketing

An invitee opens the per-campaign landing page (/{slug}), enters their referral code, and a valid referral within the RSVP window mints a booking code + QR ticket.

The submit_rsvp RPC

POST /api/public/rsvp runs the submitRsvpEffect pipeline, which calls the submit_rsvp Postgres RPC. The RPC:

  • validates campaign status and the RSVP window,
  • locks the invitee row FOR UPDATE,
  • creates the RSVP + ticket + activity log in one transaction.

Re-submitting the same referral returns the existing ticket instead of duplicating — safe against double-clicks and retries. See the full contract in Public API.

Ticket view

The minted ticket is viewable at /{slug}/ticket/{booking_code} showing the QR payload and booking code.

Officer redemption

Officers open the mobile PWA at /officer/{slug}, log in with a 6-digit PIN, then scan a QR or type a booking code.

Flow

StepEndpointResult
LoginPOST /api/officer/:slug/loginSession cookie
ValidatePOST /api/officer/:slug/validate-codeTicket details (masked name, qty) or duplicate/invalid/blocked
RedeemPOST /api/officer/:slug/redeemStatus → redeemed
HistoryGET /api/officer/:slug/historyRecent redemptions + stats

Every attempt — success, duplicate, invalid, blocked — is written to redemptions and scan_attempt_logs, and mirrored into the Activity Log. A duplicate attempt records the previous officer and time.

Notifications

revento sends transactional messages on three events: invitation, RSVP response, and redeem-complete. Channels are resolved per tenant via notification_channel_options; every send is recorded in notification_logs with per-channel delivery status.

ChannelProviderNotes
EmailResendRich-text body + parameter-tag chips, rendered inside a branded revento email frame (centered logo, ticket-style card, campaign logo for campaign messages)
WhatsAppWAHASelf-hosted WhatsApp HTTP API
Ops alertsTelegramInternal operational notifications

The per-tenant channel mode is one of email_only or email_whatsapp; per-channel delivery status is success, failed, or skipped.

Email template defaults

Default email bodies for all three transactional events are editable at Settings → Notification Templates. Templates are stored per-tenant in app_settings under the key campaign_notification_templates and pre-fill Step 4 of the campaign wizard when a new campaign is created. Each template supports a rich-text body editor with inline selection formatting, subject field, live preview, and parameter tag chips ({{invitee_name}}, {{campaign_name}}, etc.). Onboarding email templates (admin / officer magic-link flows) are managed separately at Settings → Onboarding Messages.

Campaign report

From the campaign dashboard, the AI Report CTA exports an official campaign report PDF via GET /api/admin/:slug/report.

How it works

  • The route is admin-guarded and runs on the Node.js runtime (force-dynamic).
  • Metrics are aggregated in campaign-report.ts.
  • An optional insight is fetched from ai-insight.ts (OpenAI-compatible, server-only).
  • The document is rendered to a PDF buffer by components/reports/campaign-report-document.tsx using @react-pdf/renderer.

AI insight degrades gracefully — if AI_API_KEY is missing or the call fails, the PDF still renders without the insight section. The CTA is disabled when a campaign has no invitees.

API Reference

Overview

revento exposes HTTP route handlers under /api for the public RSVP surface, the officer PWA, and a few admin operations. All dashboard CRUD goes through Server Actions, not these endpoints.

AspectConvention
Base URLSame origin as the app (e.g. https://revento.online)
Content typeapplication/json for request + response, unless noted (assets = multipart/form-data, report = application/pdf)
Path params:slug is the campaign slug; :booking_code is the ticket booking code
AuthhttpOnly session cookies (admin / officer). No public API keys are issued.
Success shapeEndpoint-specific JSON (documented per endpoint)
Error shape{ "error": "<code>" } with an HTTP status (assets use a human message)

Endpoints are not versioned by URL. Breaking API changes follow the SemVer policy (see Releases & versioning).

Authentication

Two independent cookie-based sessions, plus single-use magic-link tokens.

SessionCookieIssued byTTL
Admin / dashboardrevento-admin-sessionLogin Server Action (email + password, bcrypt)8h, or 30d with “stay logged in”
Officerrevento-officer-sessionPOST /api/officer/:slug/login (6-digit PIN)8h
Magic link— (URL token)issueToken() → emailed linkSingle-use, expiry-bound, SHA-256 hashed
  • Admin sessions are JWT (HS256, signed with AUTH_SECRET). src/proxy.ts verifies the cookie for every /admin/* request and admin API routes call requireAdminSession().
  • Officer endpoints (except login/logout) require the revento-officer-session cookie; a missing cookie returns 401 session_invalid.
  • Magic links back admin onboarding, admin password reset, and officer PIN reset; tokens are superseded on re-issue.

Public API

Unauthenticated endpoints used by the public RSVP landing page.

POST/api/public/rsvp

Submit a referral code to mint (or re-fetch) a ticket. Idempotent per referral.

Request body

FieldTypeRequiredDescription
campaignIdstring (uuid)YesTarget campaign id
referralCodestringYesInvitee referral code
{
  "campaignId": "f2c1e0a4-...",
  "referralCode": "DEMO01"
}

200 OK

{
  "bookingCode": "RV-7QK2-9F3D",
  "qrPayload": "rv:tkt:f2c1...:RV-7QK2-9F3D",
  "quantity": 2,
  "inviteeName": "Andi Wijaya",
  "alreadySubmitted": false
}

alreadySubmitted is true when the referral was already used and the existing ticket is returned.

Errors

StatuserrorCause
400invalid_requestMissing campaignId or referralCode
404campaign_not_foundUnknown campaign id
422invalid_referralReferral code not recognized
422used_referralReferral already consumed (non-idempotent path)
400campaign_paused / campaign_closedCampaign not accepting RSVPs
400rsvp_not_open / rsvp_closedOutside the RSVP window
400database_errorUnexpected RPC / DB failure

GET/api/public/campaigns/:slug

Fetch the public campaign record for a landing page by slug.

200 OK

Returns the full Campaign object (see Data schemas → Campaign). Abridged example:

{
  "id": "f2c1e0a4-...",
  "tenantId": "9a0b-...",
  "name": "Indonesia Investment & Mining Expo 2026",
  "slug": "IIMX2026",
  "status": "active",
  "eventStartAt": "2026-05-23T02:00:00.000Z",
  "eventEndAt": "2026-05-23T10:00:00.000Z",
  "venueName": "JCC Senayan, Jakarta",
  "rsvpStartAt": "2026-05-01T00:00:00.000Z",
  "rsvpEndAt": "2026-05-22T17:00:00.000Z",
  "redemptionStartAt": "2026-05-23T01:00:00.000Z",
  "redemptionEndAt": "2026-05-23T11:00:00.000Z",
  "landingLogoUrl": "https://.../logos/...",
  "landingBannerUrls": ["https://.../banners/..."],
  "landingThemeKey": "corporate",
  "ticketQuantityPerRsvp": 2,
  "notificationMode": "email_whatsapp",
  "updatedAt": "2026-06-04T06:00:00.000Z"
}

Errors

StatuserrorCause
404not_foundNo campaign with that slug

Officer API

Mobile PWA endpoints. All except login / logout require the revento-officer-session cookie.

POST/api/officer/:slug/login

Authenticate an officer with a 6-digit PIN; sets the session cookie.

Request body

FieldTypeRequiredDescription
pinstringYesExactly 6 digits (^\d{6}$)
{ "pin": "123456" }

200 OK

Sets revento-officer-session (httpOnly, 8h) and returns:

{
  "campaign": { "id": "f2c1...", "name": "IIMX 2026", "slug": "IIMX2026" },
  "officer":  { "id": "0b7e...", "name": "Budi", "email": "budi@revento.id" },
  "expiresAt": "2026-05-23T10:00:00.000Z",
  "source": "db"
}

Errors

StatuserrorCause
400invalid_requestPIN is not exactly 6 digits
404campaign_not_foundUnknown campaign slug
401invalid_pinPIN does not match any officer
401officer_inactiveOfficer is inactive / suspended
500database_errorUnexpected DB failure

POST/api/officer/:slug/logout

Clear the officer session cookie. Always succeeds.

200 OK

{ "ok": true }

POST/api/officer/:slug/validate-code

Validate a booking code / QR before redeeming. Returns a discriminated result union without changing ticket state.

Request body

FieldTypeRequiredDescription
bookingCodestringYesBooking code or decoded QR payload
method"manual" | "qr"NoInput method (default "manual")
{ "bookingCode": "RV-7QK2-9F3D", "method": "qr" }

200 OK — result variants

// valid
{ "result": "valid", "source": "db", "ticketId": "...",
  "name": "A. Wijaya", "bookingCode": "RV-7QK2-9F3D", "quantity": 2 }

// duplicate (already redeemed)
{ "result": "duplicate", "source": "db", "ticketId": "...",
  "name": "A. Wijaya", "bookingCode": "RV-7QK2-9F3D",
  "redeemedAt": "2026-05-23T03:11:00.000Z", "prevOfficer": "Budi" }

// invalid (unknown code)
{ "result": "invalid", "source": "db",
  "bookingCode": "RV-0000-0000", "message": "Ticket not found" }

// blocked (paused/closed/out-of-window)
{ "result": "blocked", "source": "db", "bookingCode": "RV-7QK2-9F3D",
  "reason": "redemption_closed", "message": "Redemption window is closed" }

Errors

StatuserrorCause
401session_invalidMissing / unrecognized officer cookie
401session_expiredOfficer session expired
400invalid_requestMissing bookingCode
404campaign_not_foundUnknown campaign slug
500database_errorUnexpected DB failure

POST/api/officer/:slug/redeem

Redeem a ticket. Same request body and result union as validate-code; on success the ticket status becomes redeemed and the attempt is logged. Duplicate / invalid / blocked attempts are also recorded.

{ "bookingCode": "RV-7QK2-9F3D", "method": "manual" }

Errors are identical to validate-code.

GET/api/officer/:slug/history

Recent scan attempts for the signed-in officer's campaign, plus aggregate stats.

200 OK

{
  "entries": [
    {
      "id": "re_01...",
      "name": "A. Wijaya",
      "bookingCode": "RV-7QK2-9F3D",
      "time": "2026-05-23T03:11:00.000Z",
      "inputMethod": "qr",
      "officer": "Budi",
      "status": "success"
    }
  ],
  "stats": { "success": 12, "duplicate": 1, "invalid": 0, "blocked": 2 }
}

status is one of success, duplicate, invalid, blocked. Errors match the other authenticated officer endpoints.

Admin API

Admin-guarded endpoints. Require a valid revento-admin-session cookie (enforced by requireAdminSession()).

GET/api/admin/:slug/report

Render the official campaign report as a PDF (with optional AI insight). Runs on the Node.js runtime, never cached.

200 OK

HeaderValue
Content-Typeapplication/pdf
Content-Dispositionattachment; filename="revento-report-{slug}-{YYYY-MM-DD}.pdf"
Cache-Controlno-store

Body is the binary PDF.

Errors

StatuserrorCause
401unauthorizedNo / invalid admin session
404campaign_not_foundUnknown campaign slug

POST/api/admin/assets

Upload landing / campaign image assets to Supabase Storage (bucket campaign-assets) and get back public URLs.

Request — multipart/form-data

FieldTypeConstraints
folderstring"banners" or "logos"
filesFile[]1–8 files; image/jpeg, png, webp, gif; ≤ 5 MB each

200 OK

{
  "urls": [
    "https://<project>.supabase.co/storage/v1/object/public/campaign-assets/logos/1717.....png"
  ]
}

Errors

Statuserror (message)Cause
400Invalid asset folder.folder not banners/logos
400Invalid file count.0 or > 8 files
400Only image uploads are allowed.Disallowed MIME type
400Image size must be 5 MB or less.File too large
401Unauthorized.No / invalid admin session
500Upload failed: …Storage upload error

Unlike the JSON endpoints, the assets endpoint returns a human-readable error message rather than a machine code.

Error codes

JSON endpoints fail with { "error": "<code>" } and an HTTP status. Consolidated reference:

CodeTypical statusMeaning
invalid_request400Malformed or missing body fields
campaign_not_found404Unknown campaign id / slug
not_found404Public campaign lookup miss
invalid_referral422Referral code not recognized
used_referral422Referral already consumed
campaign_paused400Campaign paused
campaign_closed400Campaign closed
rsvp_not_open400Before the RSVP window
rsvp_closed400After the RSVP window
invalid_pin401Officer PIN mismatch
officer_inactive401Officer not active
session_invalid401Missing / bad session cookie
session_expired401Session past expiry
unauthorized401Admin session required
database_error400 / 500Unexpected DB / RPC failure

In addition, redemption results carry a blocked reason in the 200 body (e.g. redemption_not_open, redemption_closed) rather than an HTTP error.

Data schemas

TypeScript domain shapes (camelCase) returned by the API. Source: src/lib/types/domain.ts.

Campaign

FieldTypeNotes
idstring (uuid)Primary key
tenantIdstring (uuid)Owning tenant
namestringDisplay name
slugstringUnique URL slug
statusCampaignStatusscheduled | active | paused | closed | archived
eventStartAt / eventEndAtstring (ISO)Event window
venueName / mapsUrlstringVenue + map link
venueLat / venueLngnumber?Optional coordinates
rsvpStartAt / rsvpEndAtstring (ISO)RSVP window
redemptionStartAt / redemptionEndAtstring (ISO)Redemption window
landingLogoUrlstringLanding logo
landingBannerUrlsstring[]Landing banners
landingThemeKeyLandingThemeKeyLanding theme
ticketQuantityPerRsvpnumberSeats per ticket
invitationSubject / invitationTemplatestringInvitation email
rsvpResponseSubject / rsvpResponseTemplatestringRSVP email
redeemCompleteSubject / redeemCompleteTemplatestringRedeem-complete email
notificationModeNotificationModeemail_only | email_whatsapp
updatedAtstring (ISO)Last update

Ticket

FieldTypeNotes
idstring (uuid)Primary key
campaignId / inviteeId / rsvpIdstring (uuid)Relations
bookingCodestringHuman-readable code
qrPayloadstringEncoded QR payload
quantitynumberSeats
statusTicketStatusactive | redeemed | revoked | expired
generatedAtstring (ISO)Mint time
redeemedAtstring (ISO)?Set on redemption
redeemedByOfficerIdstring (uuid)?Redeeming officer

Enumerations

EnumValues
CampaignStatusscheduled · active · paused · closed · archived
TicketStatusactive · redeemed · revoked · expired
RsvpStatussubmitted · blocked
ReferralStatusunused · used · revoked
InviteeStatusactive · inactive · revoked
OfficerStatusactive · inactive · suspended · deleted
RedemptionStatussuccess · duplicate_attempt · invalid · blocked
NotificationModeemail_only · email_whatsapp
NotificationDeliveryStatussuccess · failed · skipped
RoleKeysuper_admin · admin · admin_tenant · admin_tenant_ops · officer

Reference

Routing map

RouteSurfaceGuard
/Marketing sitePublic
/{slug}Public RSVP landingPublic
/{slug}/ticket/{booking_code}Public ticket viewPublic
/loginAdmin loginPublic
/onboarding/admin/{token}Admin magic-link onboardingToken
/officer/{slug}Officer PWAPIN
/officer/onboarding/{token}Officer PIN onboardingToken
/admin/*Internal dashboardJWT (proxy.ts)
/documentationDocumentationPublic
/robots.txtCrawler policyPublic
/sitemap.xmlPublic sitemapPublic

Any unauthenticated /admin/* request is redirected to /login?next=… by src/proxy.ts.

The canonical production origin is https://revento.online. Shared SEO helpers live in src/lib/seo.ts so canonical URLs, absolute image URLs, trimmed descriptions, and campaign RSVP share text stay consistent across metadata routes.

SurfaceSearch policyMetadata behavior
Marketing /IndexableCanonical URL, Open Graph, Twitter card, keywords, and JSON-LD structured data
/documentationPublic, included in sitemapListed in /sitemap.xml as a support surface
Campaign RSVP /{slug}noindex, nofollowDynamic title, description, canonical URL, and social image from campaign data
/admin, /login, /officer, /apiDisallowedBlocked in /robots.txt and omitted from /sitemap.xml

RSVP pages are optimized for accurate link previews, not Google indexing. Their social image fallback order is first landing banner, then campaign logo, then generic revento branding.

Environment variables

Create .env.local with:

VariablePurpose
NEXT_PUBLIC_SUPABASE_URLSupabase project URL (public)
NEXT_PUBLIC_SUPABASE_ANON_KEYSupabase anon key (browser client)
SUPABASE_SERVICE_ROLE_KEYService-role key (server-only — never expose)
AUTH_SECRETSecret for signing JWT session cookies (HS256)
RESEND_API_KEYResend API key for transactional email
RESEND_FROM_EMAILResend sender address
WAHA_BASE_URLWAHA (WhatsApp HTTP API) base URL
WAHA_API_KEYWAHA API key
WAHA_SESSION_NAMEWAHA session name
TELEGRAM_BOT_TOKENTelegram bot token (ops alerts)
TELEGRAM_CHAT_IDTelegram chat ID (ops alerts)
AI_API_KEYAI insight provider key (optional — omit to disable AI insight)
AI_BASE_URLOpenAI-compatible API base (default https://api.deepseek.com)
AI_MODELModel id (default deepseek-v4-flash)

AI insight is provider-agnostic — point AI_* at DeepSeek, OpenAI, OpenRouter, Together, Groq, or a local gateway. Legacy DEEPSEEK_* names are read as a fallback.

Scripts

ScriptAction
npm run devStart the dev server (Turbopack)
npm run dev:stableDev server with file polling on port 3001 (webpack)
npm run buildProduction build
npm run startStart the production server
npm run lintESLint
npm run typechecktsc --noEmit
npm run test:e2ePlaywright E2E suite

E2E tests are organized by scenario (tests/scenarios/<domain>/<action>.spec.ts); the canonical runs use --workers=1 because fixtures are shared and self-cleaning around the IIMX2026 campaign.

Releases & versioning

revento follows Semantic Versioning (MAJOR.MINOR.PATCH). The version is bumped on every production deploy (dev → main merge), sized to the PR scope.

BumpWhen
MAJOR (X.0.0)Breaking change, a new product surface, or a large overhaul
MINOR (1.X.0)New feature / capability, backward compatible
PATCH (1.0.X)Bug fix, copy/style/a11y/perf polish, no new capability

The number is single-sourced from package.json and surfaced in the UI via src/lib/constants/version.ts (APP_VERSION) — shown in the dashboard sidebar footer and the marketing footer. Full history lives in CHANGELOG.md.

Each release: bump package.json, add a dated CHANGELOG.md entry, merge to main, tag the merge commit (git tag vX.Y.Z), and create a GitHub Release. Vercel auto-deploys main.

Language per surface

revento keeps language tied to audience. Internal dashboard and documentation copy stay English. Invitee/officer-facing surfaces (Public RSVP and Officer PWA) stay Bahasa Indonesia, as do campaign notification templates. Shared copy maps live under src/lib/i18n/ to keep this convention visible and testable.

Documentation versioning

This documentation carries its own version — currently v1.2.0 — independent of the app APP_VERSION. It is single-sourced from src/app/documentation/version.ts (DOCS_VERSION) and shown in the docs topbar and footer. Bump it on meaningful content or structure changes, not on every app deploy.

BumpWhen
MAJORRestructure or remove sections
MINORNew section or major content addition
PATCHFixes, clarifications, small additions

Documentation changelog

  • v1.1.0 — added SEO and search-policy documentation, including crawler routes, public sitemap scope, and RSVP noindex behavior.
  • v1.0.0 — initial documentation: getting started, concepts, guides, full API reference (auth, public / officer / admin endpoints with request/response contracts, error codes, data schemas), and reference.

© 2026 revento. Internal product documentation · Docs v1.2.0