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
| Capability | Description |
|---|---|
| Multi-tenancy | Each tenant owns its campaigns, invitees, officers, and notification channels. |
| Campaign management | A guided wizard creates campaigns: event window, venue + map, RSVP window, redemption window, landing assets, and notification templates. |
| Invitee + referral | Invitees 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 RSVP | Invitees RSVP on a per-campaign landing page; a valid referral mints a booking code + QR ticket. |
| Onsite redemption | Officers 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. |
| Notifications | Transactional email (Resend) and WhatsApp (WAHA) on invitation, RSVP response, and redeem-complete. |
| Campaign report | One-click official report exported as a server-rendered PDF, with an optional AI-generated insight. |
| RBAC | Four admin roles plus officers, with tenant- and campaign-scoped access. |
| Auditing | Activity 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(apackage-lock.jsonis 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 devOpen http://localhost:3000.
Demo accounts
The demo seed provisions a sample tenant + campaign and an account for every role.
| Surface | Where | Credentials |
|---|---|---|
| Dashboard (super admin) | /login | super@revento.id / Qwerty123! |
| Dashboard (admin) | /login | admin@revento.id / Qwerty123! |
| Dashboard (tenant) | /login | admin-tenant@revento.id / Qwerty123! |
| Dashboard (tenant ops) | /login | admin-tenant-ops@revento.id / Qwerty123! |
| Public RSVP | /IIMX2026 | referral code DEMO01 |
| Officer PWA | /officer/IIMX2026 | PIN 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
| Area | Choice |
|---|---|
| Framework | Next.js 16.2.6 (App Router, RSC, Server Actions) |
| UI runtime | React 19.2.4 |
| Styling | Tailwind CSS v4, next-themes |
| Dashboard UI | SmoothUI + Radix Slot, CVA, clsx, tailwind-merge |
| Animation | motion · lenis (marketing smooth scroll) |
| Database | Supabase (Postgres) via @supabase/supabase-js |
| Domain logic | effect (typed errors + transactional pipelines) |
| Auth | jose (JWT HS256), bcryptjs (password + officer PIN hashing) |
| resend | |
| PDF reports | @react-pdf/renderer (server-rendered) |
| AI insight | Any OpenAI-compatible Chat Completions API (DeepSeek default) |
| Testing | Playwright |
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.
| Surface | Audience | Route prefix | Styling |
|---|---|---|---|
| Internal Dashboard | Super admins, admins, tenant ops | /admin/* | SmoothUI + Tailwind tokens, dark default, JWT-guarded |
| Public RSVP | Invitees | /{slug} | Custom landing CSS + motion, always light |
| Officer PWA | Onsite officers | /officer/{slug} | 100% inline styles, mobile-first |
| Marketing site | Visitors | / | 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
tenantsowncampaignsand configurenotification_channel_options.campaignshaveinviteesand are staffed byofficers.- An
inviteesubmits onersvp, which mints oneticket. - A
ticketis redeemed viaredemptionsperformed byofficers. - Every flow writes to
activity_logs,notification_logs, orscan_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.
| Role | Campaign access |
|---|---|
| super_admin | Everything |
| admin | Everything |
| admin_tenant | Campaigns whose tenantId is in assignedTenantIds (empty campaign list = all current + future campaigns of the tenant) |
| admin_tenant_ops | Tenant match AND campaign in assignedCampaignIds |
| officer | No 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 → archivedA 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
- Basics — name, unique slug, tenant, event date window.
- Venue — address and map coordinates.
- Windows — RSVP open/close and redemption open/close.
- Notifications — rich-text email subject + body templates (alignment, font size/weight) with parameter-tag chips.
- 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
| Step | Endpoint | Result |
|---|---|---|
| Login | POST /api/officer/:slug/login | Session cookie |
| Validate | POST /api/officer/:slug/validate-code | Ticket details (masked name, qty) or duplicate/invalid/blocked |
| Redeem | POST /api/officer/:slug/redeem | Status → redeemed |
| History | GET /api/officer/:slug/history | Recent 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.
| Channel | Provider | Notes |
|---|---|---|
| Resend | Rich-text body + parameter-tag chips, rendered inside a branded revento email frame (centered logo, ticket-style card, campaign logo for campaign messages) | |
| WAHA | Self-hosted WhatsApp HTTP API | |
| Ops alerts | Telegram | Internal 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.tsxusing@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.
| Aspect | Convention |
|---|---|
| Base URL | Same origin as the app (e.g. https://revento.online) |
| Content type | application/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 |
| Auth | httpOnly session cookies (admin / officer). No public API keys are issued. |
| Success shape | Endpoint-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.
| Session | Cookie | Issued by | TTL |
|---|---|---|---|
| Admin / dashboard | revento-admin-session | Login Server Action (email + password, bcrypt) | 8h, or 30d with “stay logged in” |
| Officer | revento-officer-session | POST /api/officer/:slug/login (6-digit PIN) | 8h |
| Magic link | — (URL token) | issueToken() → emailed link | Single-use, expiry-bound, SHA-256 hashed |
- Admin sessions are JWT (HS256, signed with
AUTH_SECRET).src/proxy.tsverifies the cookie for every/admin/*request and admin API routes callrequireAdminSession(). - Officer endpoints (except login/logout) require the
revento-officer-sessioncookie; a missing cookie returns401 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
| Field | Type | Required | Description |
|---|---|---|---|
campaignId | string (uuid) | Yes | Target campaign id |
referralCode | string | Yes | Invitee 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
| Status | error | Cause |
|---|---|---|
| 400 | invalid_request | Missing campaignId or referralCode |
| 404 | campaign_not_found | Unknown campaign id |
| 422 | invalid_referral | Referral code not recognized |
| 422 | used_referral | Referral already consumed (non-idempotent path) |
| 400 | campaign_paused / campaign_closed | Campaign not accepting RSVPs |
| 400 | rsvp_not_open / rsvp_closed | Outside the RSVP window |
| 400 | database_error | Unexpected 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
| Status | error | Cause |
|---|---|---|
| 404 | not_found | No 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
| Field | Type | Required | Description |
|---|---|---|---|
pin | string | Yes | Exactly 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
| Status | error | Cause |
|---|---|---|
| 400 | invalid_request | PIN is not exactly 6 digits |
| 404 | campaign_not_found | Unknown campaign slug |
| 401 | invalid_pin | PIN does not match any officer |
| 401 | officer_inactive | Officer is inactive / suspended |
| 500 | database_error | Unexpected 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
| Field | Type | Required | Description |
|---|---|---|---|
bookingCode | string | Yes | Booking code or decoded QR payload |
method | "manual" | "qr" | No | Input 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
| Status | error | Cause |
|---|---|---|
| 401 | session_invalid | Missing / unrecognized officer cookie |
| 401 | session_expired | Officer session expired |
| 400 | invalid_request | Missing bookingCode |
| 404 | campaign_not_found | Unknown campaign slug |
| 500 | database_error | Unexpected 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
| Header | Value |
|---|---|
| Content-Type | application/pdf |
| Content-Disposition | attachment; filename="revento-report-{slug}-{YYYY-MM-DD}.pdf" |
| Cache-Control | no-store |
Body is the binary PDF.
Errors
| Status | error | Cause |
|---|---|---|
| 401 | unauthorized | No / invalid admin session |
| 404 | campaign_not_found | Unknown 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
| Field | Type | Constraints |
|---|---|---|
folder | string | "banners" or "logos" |
files | File[] | 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
| Status | error (message) | Cause |
|---|---|---|
| 400 | Invalid asset folder. | folder not banners/logos |
| 400 | Invalid file count. | 0 or > 8 files |
| 400 | Only image uploads are allowed. | Disallowed MIME type |
| 400 | Image size must be 5 MB or less. | File too large |
| 401 | Unauthorized. | No / invalid admin session |
| 500 | Upload 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:
| Code | Typical status | Meaning |
|---|---|---|
invalid_request | 400 | Malformed or missing body fields |
campaign_not_found | 404 | Unknown campaign id / slug |
not_found | 404 | Public campaign lookup miss |
invalid_referral | 422 | Referral code not recognized |
used_referral | 422 | Referral already consumed |
campaign_paused | 400 | Campaign paused |
campaign_closed | 400 | Campaign closed |
rsvp_not_open | 400 | Before the RSVP window |
rsvp_closed | 400 | After the RSVP window |
invalid_pin | 401 | Officer PIN mismatch |
officer_inactive | 401 | Officer not active |
session_invalid | 401 | Missing / bad session cookie |
session_expired | 401 | Session past expiry |
unauthorized | 401 | Admin session required |
database_error | 400 / 500 | Unexpected 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
| Field | Type | Notes |
|---|---|---|
id | string (uuid) | Primary key |
tenantId | string (uuid) | Owning tenant |
name | string | Display name |
slug | string | Unique URL slug |
status | CampaignStatus | scheduled | active | paused | closed | archived |
eventStartAt / eventEndAt | string (ISO) | Event window |
venueName / mapsUrl | string | Venue + map link |
venueLat / venueLng | number? | Optional coordinates |
rsvpStartAt / rsvpEndAt | string (ISO) | RSVP window |
redemptionStartAt / redemptionEndAt | string (ISO) | Redemption window |
landingLogoUrl | string | Landing logo |
landingBannerUrls | string[] | Landing banners |
landingThemeKey | LandingThemeKey | Landing theme |
ticketQuantityPerRsvp | number | Seats per ticket |
invitationSubject / invitationTemplate | string | Invitation email |
rsvpResponseSubject / rsvpResponseTemplate | string | RSVP email |
redeemCompleteSubject / redeemCompleteTemplate | string | Redeem-complete email |
notificationMode | NotificationMode | email_only | email_whatsapp |
updatedAt | string (ISO) | Last update |
Ticket
| Field | Type | Notes |
|---|---|---|
id | string (uuid) | Primary key |
campaignId / inviteeId / rsvpId | string (uuid) | Relations |
bookingCode | string | Human-readable code |
qrPayload | string | Encoded QR payload |
quantity | number | Seats |
status | TicketStatus | active | redeemed | revoked | expired |
generatedAt | string (ISO) | Mint time |
redeemedAt | string (ISO)? | Set on redemption |
redeemedByOfficerId | string (uuid)? | Redeeming officer |
Enumerations
| Enum | Values |
|---|---|
| CampaignStatus | scheduled · active · paused · closed · archived |
| TicketStatus | active · redeemed · revoked · expired |
| RsvpStatus | submitted · blocked |
| ReferralStatus | unused · used · revoked |
| InviteeStatus | active · inactive · revoked |
| OfficerStatus | active · inactive · suspended · deleted |
| RedemptionStatus | success · duplicate_attempt · invalid · blocked |
| NotificationMode | email_only · email_whatsapp |
| NotificationDeliveryStatus | success · failed · skipped |
| RoleKey | super_admin · admin · admin_tenant · admin_tenant_ops · officer |
Reference
Routing map
| Route | Surface | Guard |
|---|---|---|
/ | Marketing site | Public |
/{slug} | Public RSVP landing | Public |
/{slug}/ticket/{booking_code} | Public ticket view | Public |
/login | Admin login | Public |
/onboarding/admin/{token} | Admin magic-link onboarding | Token |
/officer/{slug} | Officer PWA | PIN |
/officer/onboarding/{token} | Officer PIN onboarding | Token |
/admin/* | Internal dashboard | JWT (proxy.ts) |
/documentation | Documentation | Public |
/robots.txt | Crawler policy | Public |
/sitemap.xml | Public sitemap | Public |
Any unauthenticated /admin/* request is redirected to /login?next=… by src/proxy.ts.
SEO & search policy
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.
| Surface | Search policy | Metadata behavior |
|---|---|---|
| Marketing / | Indexable | Canonical URL, Open Graph, Twitter card, keywords, and JSON-LD structured data |
| /documentation | Public, included in sitemap | Listed in /sitemap.xml as a support surface |
| Campaign RSVP /{slug} | noindex, nofollow | Dynamic title, description, canonical URL, and social image from campaign data |
| /admin, /login, /officer, /api | Disallowed | Blocked 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:
| Variable | Purpose |
|---|---|
NEXT_PUBLIC_SUPABASE_URL | Supabase project URL (public) |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Supabase anon key (browser client) |
SUPABASE_SERVICE_ROLE_KEY | Service-role key (server-only — never expose) |
AUTH_SECRET | Secret for signing JWT session cookies (HS256) |
RESEND_API_KEY | Resend API key for transactional email |
RESEND_FROM_EMAIL | Resend sender address |
WAHA_BASE_URL | WAHA (WhatsApp HTTP API) base URL |
WAHA_API_KEY | WAHA API key |
WAHA_SESSION_NAME | WAHA session name |
TELEGRAM_BOT_TOKEN | Telegram bot token (ops alerts) |
TELEGRAM_CHAT_ID | Telegram chat ID (ops alerts) |
AI_API_KEY | AI insight provider key (optional — omit to disable AI insight) |
AI_BASE_URL | OpenAI-compatible API base (default https://api.deepseek.com) |
AI_MODEL | Model 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
| Script | Action |
|---|---|
npm run dev | Start the dev server (Turbopack) |
npm run dev:stable | Dev server with file polling on port 3001 (webpack) |
npm run build | Production build |
npm run start | Start the production server |
npm run lint | ESLint |
npm run typecheck | tsc --noEmit |
npm run test:e2e | Playwright 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.
| Bump | When |
|---|---|
| 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.
| Bump | When |
|---|---|
| MAJOR | Restructure or remove sections |
| MINOR | New section or major content addition |
| PATCH | Fixes, 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.