revento documentation
Introduction
revento is a multi-tenant event RSVP, ticketing, and onsite redemption platform. Organizations run invitation-only or open-registration events end to end: brands 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-brand operations | Each Client owns one or more Brands; each Brand owns its campaigns, invitees, officers, and notification channels. |
| Campaign management | A six-step wizard configures event basics, invitation-only or open access, ticketing, a flexible RSVP form, landing assets, and notifications. |
| Invitees / registrants | Invitation-only campaigns support manual and CSV invitees plus referral delivery. Open-registration campaigns receive participants only from the public form and expose response detail plus CSV export. |
| Public RSVP | Every landing theme renders the same campaign form schema. Invitation-only RSVP validates a referral; open registration deduplicates normalized email. Both issue a 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 (Meta WhatsApp Cloud API) 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 | Revento Team, Client Team, and Campaign Level access plus officers, with server-enforced Client and Campaign scope. |
| 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 brand + 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 (Client Team) | /login | admin-tenant@revento.id / Qwerty123! |
| Dashboard (Campaign Level) | /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 (dashboard + RSVP microinteractions) · lenis (marketing smooth scroll) · CSS/IntersectionObserver (marketing reveals) |
| 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 | Revento Team, Client Team, Campaign Level | /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 + 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 — 17 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
clientscontain one or more Brands, stored in the legacytenantstable.- Brands own
campaignsand configurenotification_channel_optionsthrough the internaltenant_idforeign key. admin_campaign_assignmentsnormalizes Campaign Level scope while legacy arrays remain during rollout.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
clients, 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, admin_campaign_assignments.
Brand is the product term. Database tables, role keys, analytics groups, and API compatibility fields retain the legacy tenant name to avoid a breaking migration.
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
The server resolves one access context for navigation, routes, Server Actions, and campaign operations. The stored role keys remain stable during migration.
| Role | Campaign access |
|---|---|
| super_admin / admin | Revento Team: every Client, Brand, and Campaign |
| admin_tenant | Client Team: every current and future Brand and Campaign in one clientId |
| admin_tenant_ops | Campaign Level: normalized Campaign assignments inside one clientId |
| officer | No dashboard; PIN-scoped to one campaign's redemption |
Revento Team sees Dashboard, Client, Brand, Admin, Campaign, RBAC, and Setting. Client Team sees Dashboard, Brand, Admin, and Campaign. Campaign Level sees Dashboard, Invitee / Registrant, and Officer. Action templates can be overridden per user but cannot expand scope.
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
- Basic Info — name, unique slug, Brand, venue, and event/RSVP windows.
- Access & Ticketing — choose invitation-only or open registration and set ticket quantity.
- RSVP Form — compose, reorder, validate, and preview system/custom fields.
- Assets — choose the landing theme, logos, and banners.
- Notifications — configure channels and rich-text templates when the selected mode needs them.
- Review — confirm the complete campaign before saving.
Access mode and form structure remain editable until the first RSVP. After that, both are locked so stored responses keep the same meaning.
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
A guest opens the per-campaign landing page (/{slug}) and completes the campaign form. Invitation-only campaigns also require a referral code; open-registration campaigns create a registrant directly. Both modes mint a booking code + QR ticket.
The submit_campaign_rsvp RPC
POST /api/public/rsvp runs the submitRsvpEffect pipeline, which calls the submit_campaign_rsvp Postgres RPC. The RPC:
- validates campaign status and the RSVP window,
- validates the versioned form schema and submitted answers,
- locks an invitee or creates a deduplicated registrant,
- stores answers and consent, then creates RSVP + ticket + activity log atomically.
Re-submitting the same submission key 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
| 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 |
| Manifest | GET /api/officer/:slug/manifest | Per-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 Brand 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) | |
| Meta WhatsApp Cloud API | Direct provider send path; test fixtures may use mock_whatsapp | |
| Ops alerts | Telegram | Internal operational notifications |
The per-Brand 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-Brand 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.tsfrom 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.tsxusing@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.
| 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; :ticket_token is the signed public ticket token |
| 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 campaign form answers to mint (or idempotently re-fetch) a ticket.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
campaignId | string (uuid) | Yes | Target campaign id |
referralCode | string | Invitation only | Invitee referral code |
answers | object | Yes | Values keyed by stable form field id |
submissionKey | string (uuid) | Yes | Retry-safe submission id |
turnstileToken | string | When enabled | Turnstile challenge token |
{
"campaignId": "f2c1e0a4-...",
"referralCode": "DEMO01",
"answers": {
"system-full-name": "Andi Wijaya",
"system-email": "andi@example.com",
"system-privacy-consent": true
},
"submissionKey": "d8208a8c-..."
}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 same idempotent submission is retried and the existing ticket is returned.
Errors
| Status | error | Cause |
|---|---|---|
| 400 | invalid_request | Malformed campaign, answers, or submission key |
| 404 | campaign_not_found | Unknown campaign id |
| 422 | invalid_referral | Referral code not recognized |
| 422 | used_referral | Referral already consumed (non-idempotent path) |
| 422 | validation_failed | Invalid answers; response includes fieldErrors |
| 409 | already_registered | Normalized email already registered with a different key |
| 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 an allowlisted public campaign DTO including access mode and a sanitized form schema. Notification templates and internal settings are never exposed. Abridged example:
{
"id": "f2c1e0a4-...",
"tenantId": "9a0b-...",
"name": "Indonesia Investment & Mining Expo 2026",
"slug": "IIMX2026",
"status": "active",
"accessMode": "open_registration",
"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,
"rsvpFormSchema": {
"version": 1,
"fields": [
{ "id": "system-full-name", "type": "short_text", "label": "Full name", "required": true }
]
}
}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.
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
| 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 |
validation_failed | 422 | Campaign form answers failed field validation |
already_registered | 409 | Open-registration email already has another submission |
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 Brand; legacy compatibility field |
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/{ticket_token} | Public ticket view | Public signed token |
/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) |
/admin/clients | Client management | Revento Team |
/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. 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.
| 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. 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:
| 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) |
CRON_SECRET | Secret used by Vercel Cron for scheduled background jobs |
RESEND_API_KEY | Resend API key for transactional email |
RESEND_FROM_EMAIL | Resend sender address |
RESEND_WEBHOOK_SECRET | Svix signing secret for the Resend delivery webhook (/api/webhooks/resend) |
WHATSAPP_ACCESS_TOKEN | Meta WhatsApp Cloud API access token |
WHATSAPP_PHONE_NUMBER_ID | Meta WhatsApp sender phone number id |
WHATSAPP_API_VERSION | Graph API version for WhatsApp Cloud API |
WHATSAPP_GRAPH_BASE_URL | Graph API base URL (default https://graph.facebook.com) |
WHATSAPP_WEBHOOK_VERIFY_TOKEN | Verify token for the Meta webhook subscription handshake (/api/webhooks/whatsapp GET) |
WHATSAPP_APP_SECRET | Meta app secret used to validate X-Hub-Signature-256 on incoming webhook events |
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) |
NEXT_PUBLIC_POSTHOG_KEY | PostHog project API key (optional — admin dashboard product analytics; omit to disable) |
NEXT_PUBLIC_POSTHOG_HOST | PostHog ingestion host, e.g. https://us.i.posthog.com (required if the key above is set) |
SENTRY_DSN | Sentry DSN for server-side error capture (optional — only active when NODE_ENV=production; omit to disable) |
NEXT_PUBLIC_SENTRY_DSN | Sentry 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
| 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.8.1 — 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.8.1 — renamed the user-facing Tenant workspace terminology to Brand and documented the backward-compatible internal storage contract.
- v1.8.0 — documented the Revento Team, Client Team, and Campaign Level hierarchy, Client-to-Brand ownership, normalized campaign assignment, role-aware navigation, and server-enforced template plus per-user permissions.
- v1.7.0 — documented optional invitation access, the flexible RSVP form builder, stored answers and consent, open-registration responses, idempotent ticket issuance, and mode-aware campaign reporting.
- 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
lenisfor 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/manifestendpoint. - v1.5.1 — noted that
NEXT_PUBLIC_SENTRY_DSNalso 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_DSNandNEXT_PUBLIC_SENTRY_DSNfor server- and client-side error monitoring (Sentry, Playbook 1). - v1.4.0 — documented
NEXT_PUBLIC_POSTHOG_KEYandNEXT_PUBLIC_POSTHOG_HOSTfor 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/resendand/api/webhooks/whatsappreconcile final delivery status onnotification_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.