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 (Meta WhatsApp Cloud API) 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 (dashboard + RSVP microinteractions) · lenis (marketing smooth scroll) · CSS/IntersectionObserver (marketing reveals)
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 + CSS/observer-driven motion

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/{ticket_token}. The URL token is signed server-side; the human-readable booking code is shown inside the ticket and remains valid for officer scan/manual redemption.

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
ManifestGET /api/officer/:slug/manifestPer-campaign valid-ticket list + redeemed snapshot, cached for offline use

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.

Offline mode

On login, the PWA syncs the campaign manifest (booking codes, guest names, quantities, and a redeemed snapshot) into IndexedDB. While offline, scans are checked against that manifest for a real VALID / INVALID verdict; codes already redeemed on the same device in the current offline session are flagged as duplicates immediately. Offline redeems are queued locally and re-validated against the server on reconnect, which remains the source of truth. A ticket redeemed on another device while this device is offline cannot be detected until the queue syncs.

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)
WhatsAppMeta WhatsApp Cloud APIDirect provider send path; test fixtures may use mock_whatsapp
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.

Scheduled invitation sends are processed by the GitHub Actions workflow .github/workflows/scheduled-invitations.yml, which calls /api/admin/notifications/scheduled-invitations. Production must define CRON_SECRET, and the same value must be stored as the GitHub repository secret CRON_SECRET. Invitation emails for a drain batch are sent via Resend's batch endpoint (up to 100 emails per call); if a batch call fails outright, every job in that batch is retried with the existing backoff/dead-letter policy. One-off transactional emails (RSVP confirmation, redeem complete, magic links) still use the single-send endpoint.

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 from active invitees.
  • 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.
  • The PDF excludes the activity timeline table so exported reports stay concise.

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; :ticket_token is the signed public ticket token
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.

GET/api/officer/:slug/manifest

The full list of valid booking codes for the signed-in officer's campaign, with a redeemed snapshot at sync time. The PWA caches this response in IndexedDB on login to support offline validation.

200 OK

{
  "entries": [
    { "bookingCode": "RV-7QK2-9F3D", "name": "A. Wijaya", "quantity": 2, "redeemed": false }
  ],
  "syncedAt": "2026-06-14T07:45:48.885Z"
}

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/{ticket_token}Public ticket viewPublic signed token
/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. The root favicon is served from the stable /favicon.ico metadata convention, with square brand icons for browser tabs, touch icons, and Google Search appearance.

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. For search appearance updates, submit /sitemap.xml and request indexing for https://revento.online/ in Google Search Console after deploy.

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)
CRON_SECRETSecret used by Vercel Cron for scheduled background jobs
RESEND_API_KEYResend API key for transactional email
RESEND_FROM_EMAILResend sender address
RESEND_WEBHOOK_SECRETSvix signing secret for the Resend delivery webhook (/api/webhooks/resend)
WHATSAPP_ACCESS_TOKENMeta WhatsApp Cloud API access token
WHATSAPP_PHONE_NUMBER_IDMeta WhatsApp sender phone number id
WHATSAPP_API_VERSIONGraph API version for WhatsApp Cloud API
WHATSAPP_GRAPH_BASE_URLGraph API base URL (default https://graph.facebook.com)
WHATSAPP_WEBHOOK_VERIFY_TOKENVerify token for the Meta webhook subscription handshake (/api/webhooks/whatsapp GET)
WHATSAPP_APP_SECRETMeta app secret used to validate X-Hub-Signature-256 on incoming webhook events
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)
NEXT_PUBLIC_POSTHOG_KEYPostHog project API key (optional — admin dashboard product analytics; omit to disable)
NEXT_PUBLIC_POSTHOG_HOSTPostHog ingestion host, e.g. https://us.i.posthog.com (required if the key above is set)
SENTRY_DSNSentry DSN for server-side error capture (optional — only active when NODE_ENV=production; omit to disable)
NEXT_PUBLIC_SENTRY_DSNSentry DSN for client-side error capture and session replay (same value as SENTRY_DSN; required if the server DSN is set)

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.6.2 — 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.6.2 — documented the stable root favicon convention and the Search Console follow-up for sitemap submission and homepage indexing after SEO/search-appearance fixes.
  • v1.6.1 — synced the marketing-site documentation with the current implementation: the landing page still uses lenis for smooth scroll, but most reveals and ambient motion now run through CSS plus lightweight viewport observers instead of landing-page motion wrappers.
  • v1.6.0 — documented Officer PWA offline validation: the manifest sync on login, offline VALID / INVALID / duplicate verdicts, and the new GET /api/officer/:slug/manifest endpoint.
  • v1.5.1 — noted that NEXT_PUBLIC_SENTRY_DSN also enables Sentry Session Replay, captured only on errors (replaysSessionSampleRate: 0, replaysOnErrorSampleRate: 1.0) to stay within the free-tier quota.
  • v1.5.0 — documented SENTRY_DSN and NEXT_PUBLIC_SENTRY_DSN for server- and client-side error monitoring (Sentry, Playbook 1).
  • v1.4.0 — documented NEXT_PUBLIC_POSTHOG_KEY and NEXT_PUBLIC_POSTHOG_HOST for the admin-dashboard product analytics integration (PostHog, Playbook PA-1).
  • v1.3.5 — added environment variable reference rows for the delivery-status webhook endpoints (RESEND_WEBHOOK_SECRET, WHATSAPP_WEBHOOK_VERIFY_TOKEN,WHATSAPP_APP_SECRET): /api/webhooks/resend and /api/webhooks/whatsapp reconcile final delivery status on notification_logs.
  • v1.3.4 — documented Resend batch-endpoint sending for scheduled invitation emails (up to 100/call) and the whole-batch retry/dead-letter behavior.
  • v1.3.3 — updated WhatsApp notification docs from WAHA to the Meta WhatsApp Cloud API, updated the scheduled-invitations delivery docs to the GitHub Actions workflow, and refreshed the environment variable reference (WHATSAPP_ACCESS_TOKEN, WHATSAPP_PHONE_NUMBER_ID,WHATSAPP_API_VERSION, WHATSAPP_GRAPH_BASE_URL).
  • v1.3.2 — added environment variable reference rows for scheduled notifications and providers (CRON_SECRET, RESEND_API_KEY,RESEND_FROM_EMAIL, WhatsApp, Telegram, and AI insight keys).
  • v1.3.1 — clarified campaign report PDF generation: metrics are aggregated from active invitees, and the activity timeline table is excluded from the exported PDF.
  • v1.3.0 — added the public ticket-view endpoint and path-parameter documentation to the API reference.
  • v1.2.0— added the "Email template defaults" sub-section under Notifications.
  • 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.6.2