Secure nominations, applicant access and award operations for the Global Business Excellence Awards 2026.
π Live portal Β Β·Β π Submit a nomination Β Β·Β π¬ Get help
The production portal for the Global Business Excellence Awards 2026. It gives nominees a clear, secure path from public nomination through payment proof and document upload; gives approved applicants a private self-service portal; and gives staff one focused workspace for review, communications, payments, reporting and audit history.
Live: access.gbeaward.com
Support: info@gbeaward.com Β· WhatsApp
Official site: gbeaward.com
Important
This is a production system for personal, nomination and payment information. Never commit .env, provider credentials, database exports, real nomination data or screenshots containing private information.
| For | Experience |
|---|---|
| Nominees | A four-step public nomination journey with category selection, supporting documents, payment choice and payment-proof upload. Every file is uploaded directly to private storage and every completed nomination gets a non-sequential GBE-2026-###### reference. |
| Approved applicants | Invitation-only access to applications, requested documents, payment status, messages, profile, password management and account security. A public nomination never creates an account by itself. |
| Award staff | One focused Staff workspace for applications, applicants, payments, files, communications, exports and reports. Super admins also manage award cycles, categories, staff, settings and audit activity. Staff must enrol TOTP MFA before administration access. |
| Operations | Controlled workflow transitions, reviewer assignment, change requests, payment verification, signed downloads, exports, delivery tracking, retention work and an auditable history of sensitive actions. |
flowchart LR
N[Public nomination] --> T[Turnstile + validation]
T --> U[Direct private R2 uploads]
U --> Q[Submitted nomination]
Q --> R[Staff review]
R -->|Approved| I[Secure invitation]
R -->|Changes requested| N
I --> P[Applicant portal]
P --> O[Documents, payment, messages and security]
R --> A[Audited administration]
The public form is deliberately short and guided:
- Nominee β company name or full name.
- Contact β contact details, award category, mandatory nomination statement and optional supporting documents.
- Payment β card or bank-transfer instructions and one payment-proof file.
- Confirm β declaration review, Turnstile verification and submission.
Supporting documents and payment proof are independently limited to 5 MB per file. The browser gives upload progress, cancellation and retry feedback; the server repeats validation before accepting a completion request.
| Layer | Technology | Responsibility |
|---|---|---|
| Application | Next.js 16 App Router, React 19, strict TypeScript | Public, applicant and staff routes; server actions; route handlers; streaming loading states. |
| Interface | Tailwind CSS 4, shadcn/Base UI, TanStack Table | Responsive, keyboard-accessible light-mode interface with tailored mobile/tablet layouts. |
| Data | Neon PostgreSQL, Drizzle ORM | Relational award data, workflow state, accounts, audit history, email outbox, rate limits and migrations. |
| Authentication | Better Auth + Drizzle adapter | Approval-first invitations, password recovery, session revocation and mandatory TOTP MFA for staff. |
| File storage | Cloudflare R2 + AWS SDK | Private, browser-to-R2 presigned uploads; type detection; signed downloads; lifecycle cleanup. |
| Abuse protection | Cloudflare Turnstile + PostgreSQL-backed rate limiting | Nomination verification plus an adaptive login challenge after two failed attempts; durable across instances. |
| Resend + React Email | Durable outbox, idempotent delivery, retry/backoff and signed delivery webhooks. | |
| Exports | ExcelJS, CSV and React PDF | Filtered staff exports and private applicant application summaries. |
| Hosting | Vercel + Cloudflare DNS | Production hosting, security headers and one Hobby-compatible daily cron. |
- Public routes are limited to the nomination and information pages. Turnstile checks action and expected hostname server-side. Login challenges appear only after two failed attempts for the same hashed email/IP fingerprint and are verified before a further password check.
- Private routes are protected early by
src/proxy.ts, then enforce a session and application-level profile checks in the server data-access layer. - Staff routes require a staff profile, active membership and mandatory TOTP MFA. Staff can complete the full nomination workflow; super admins additionally manage people and system configuration. The QR code and manual setup URI work with Google Authenticator, Microsoft Authenticator and compatible apps.
- Uploads go to the private R2 bucket by presigned URL. Object metadata, detected types, ownership and disposition are checked before a record becomes available.
- Programme media on
/applyis an on-demand dialog: the Facebook post/reel frames and their client bundle load only after a visitor requests them. The adjacent event-brochure action is a verified direct Google Drive download. No Facebook SDK is loaded into the nomination page; the restrictive policy permits Facebook only as a frame source for this dialog. - Sensitive operations write audit events. Administrative data is noindexed, and platform headers block framing and apply a restrictive content policy.
- Database roles are split: the runtime uses the least-privilege pooled connection; migrations use a separate direct owner connection.
src/
βββ app/ # App Router pages, layouts, loading/error boundaries and API routes
β βββ apply/ # Public guided nomination and submitted confirmation
β βββ portal/ # Private applicant portal
β βββ admin/ # Permission-aware staff workspace
β βββ auth/ # Invite acceptance, password recovery and staff MFA
β βββ api/ # Uploads, exports, health, cron and Resend webhook handlers
βββ components/ # Reusable interface, forms, uploads, admin and shared shell components
βββ config/ # Brand, navigation and role-permission definitions
βββ emails/ # React Email presentation components and templates
βββ lib/ # Environment parsing, Better Auth, Drizzle, R2 and domain helpers
βββ server/ # DAL, actions, jobs, security and business services
drizzle/migrations/ # Append-only PostgreSQL migrations
scripts/ # Environment, provider, seed and first-admin commands
tests/ # Unit, integration and Playwright end-to-end tests
public/brand/ # Versioned logo and award artwork used by the application and this README
- Bun 1.3.12 (the version pinned in
package.json) - A non-production Neon/PostgreSQL database with two distinct roles: runtime and migration owner
- Two non-production Cloudflare R2 buckets (private uploads and public assets), with browser PUT CORS configured for the local origin
- Cloudflare Turnstile test keys for local work
- A non-production Resend API key and verified sending domain when email flows are being exercised
bun install
cp .env.example .env
bun run env:verify
bun run db:migrate
bun run devOpen http://localhost:3000. The public nomination route is /apply.
Tip
The local defaults are intentionally safe for UI work. Full provider operations require the non-production values described in Environment configuration.
The seed command creates the approved 2026 categories but deliberately leaves the cycle in draft. It cannot silently open public nominations.
SEED_CYCLE_OPENS_AT=2026-01-01T00:00:00.000Z \
SEED_CYCLE_CLOSES_AT=2026-12-31T23:59:59.999Z \
bun run db:seedReview the dates, legal copy, categories, fees, payment instructions and feature flags in administration before an authorized super administrator changes the cycle status.
This command is one-time only. It refuses to run once a super administrator exists.
After securely setting all three BOOTSTRAP_ADMIN_* variables in the local environment, run:
bun run db:bootstrap-adminSign in, enrol TOTP MFA immediately, then remove all three BOOTSTRAP_ADMIN_* values from the environment. Create every later staff account from Administration β Staff.
Copy .env.example; it is the complete, non-secret contract. Keep actual values in local environment files or encrypted Vercel settings only.
| Group | Variables | Notes |
|---|---|---|
| Application | NEXT_PUBLIC_APP_URL, APP_ENV, APP_TIMEZONE, SUPPORT_EMAIL, OFFICIAL_SITE_URL |
Production URLs must use HTTPS. The portal timezone is Asia/Colombo. |
| Database | DATABASE_URL, DATABASE_URL_DIRECT |
Required. Use separate runtime and migration-owner roles; never use the direct owner URL in the Vercel runtime. |
| Authentication | BETTER_AUTH_SECRET, BETTER_AUTH_URL |
Required. The URL must exactly match NEXT_PUBLIC_APP_URL; the secret must be at least 32 characters. |
| R2 | R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ENDPOINT, R2_PRIVATE_BUCKET, R2_PUBLIC_BUCKET, R2_OBJECT_PREFIX, R2_PUBLIC_ASSET_BASE_URL |
Keep uploads/exports in the private bucket. Use R2_OBJECT_PREFIX to isolate preview, test and production objects. |
| Turnstile | NEXT_PUBLIC_TURNSTILE_SITE_KEY, TURNSTILE_SECRET_KEY, TURNSTILE_EXPECTED_HOSTNAME, TURNSTILE_APPLICATION_ACTION |
Use Cloudflareβs test keys only outside production. The portal verifies gbe_nomination_submit for nominations and gbe_login after two failed sign-ins. |
| Resend | RESEND_API_KEY, RESEND_WEBHOOK_SECRET, EMAIL_FROM, EMAIL_REPLY_TO |
EMAIL_FROM must use a verified Resend domain. Configure the signed Resend webhook at /api/webhooks/resend. |
| Operations | CRON_SECRET |
Required and at least 24 characters. Authorizes maintenance routes; Vercel invokes only /api/cron/daily on the scheduled job. |
| First admin | BOOTSTRAP_ADMIN_NAME, BOOTSTRAP_ADMIN_EMAIL, BOOTSTRAP_ADMIN_PASSWORD |
Temporary only. Remove immediately after the initial account has been created and secured. |
| Seed | SEED_CYCLE_OPENS_AT, SEED_CYCLE_CLOSES_AT |
Required only by bun run db:seed; use approved ISO 8601 timestamps. |
Run both checks after configuring an environment:
bun run env:verify
bun run providers:verifyenv:verify rejects unsafe configuration such as matching runtime/owner database URLs, production Turnstile test keys or leftover bootstrap credentials. providers:verify proves database access, runtime rate-limit permissions, private R2 read/write/delete plus browser CORS, Resend sender status and Turnstile hostname policy.
| Command | What it proves |
|---|---|
bun run lint |
ESLint passes with zero warnings. |
bun run typecheck |
Strict TypeScript has no errors. |
bun run test |
Fast unit tests for business rules, security/permissions, validation, loading boundaries, exports and PDFs. |
bun run test:integration |
Database-integrity tests against an isolated gbe_award_portal_test* database. Requires TEST_DATABASE_URL and TEST_DATABASE_ADMIN_URL. |
bun run test:e2e |
Playwright desktop/mobile journeys using a recreated gbe_award_portal_test_e2e database and e2e/playwright R2 prefix. It covers public submission, retries, file validation, adaptive login protection, accessibility, staff MFA, export and responsive overflow. |
bun run build |
Production Next.js build. |
bun run check |
Lint, typecheck, unit tests and production build in one command. |
bun audit |
Bun dependency vulnerability audit. |
Caution
Integration and end-to-end tests recreate dedicated test databases. Their names are guarded in code, but they still require a database owner connection that is allowed to create and drop only test databases. Never point test variables at a production database.
Migrations in drizzle/migrations are append-only and are the source of truth for schema history.
bun run db:generate # generate a migration, then review it carefully
bun run db:migrate # apply reviewed migrations with DATABASE_URL_DIRECT
bun run db:studio # local inspection onlyDo not run db:push against staging or production. Before any production schema change, take a Neon branch or restore point, confirm the migration is backward-compatible with the currently deployed application, apply it with the owner URL, and retain a rollback path.
Files are not stored in PostgreSQL or Vercelβs filesystem. The application stores file records and audit metadata in PostgreSQL, while original bytes live in R2. Private downloads are signed per request and exports are temporary private objects.
Application responses queue email in PostgreSQL. A Next.js after() callback attempts prompt delivery; a durable outbox retries failed messages with backoff, and Resendβs signed webhook records delivered, bounced and failed events.
| Endpoint | Purpose | Protection |
|---|---|---|
POST /api/webhooks/resend |
Records verified Resend delivery events | RESEND_WEBHOOK_SECRET signature verification |
GET /api/cron/daily |
Runs email retries, stale-upload cleanup, expired-export cleanup, retention and stale rate-limit cleanup | Bearer authorization using the configured CRON_SECRET |
GET /api/health |
Reports database reachability | No secrets returned; do not treat it as a public status page |
Vercel Hobby supports one cron entry. vercel.json schedules the daily route at 23 2 * * *; do not add overlapping Vercel cron entries for individual cleanup jobs.
- Review scope β inspect the diff, check that no secrets or production data are included, and preserve approved award copy unless the change explicitly calls for it.
- Validate locally β run
bun run check, then the relevant integration/E2E suite. Runbun run env:verifyandbun run providers:verifywith the target environment. - Prepare data safely β take a Neon restore point/branch; apply only reviewed backward-compatible migrations with
DATABASE_URL_DIRECT. - Deploy β push
main; Vercel deploys production. Wait forReadyand verify theaccess.gbeaward.comalias. - Prove critical paths β confirm
/api/health, one sign-in/MFA path, one authorized private upload/download, the current award-cycle settings, cron authorization and Resend delivery-webhook events. - Observe β use Administration β Communications and Activity to inspect delivery, workflow and operational events after release.
The deployment process never opens a cycle automatically. Opening, closing or changing award programme settings remains an explicit, authorized administrative decision.
This repository is maintained as a production application. Before changing it, read AGENTS.md for the exact project workflow, architecture boundaries, security requirements and verification expectations.
In short: use Bun, keep server/data access on the server, preserve the private-file and permission model, add migrations rather than rewriting history, test the affected route at desktop and mobile widths, and do not make external provider, deployment or award-programme changes without explicit authorization.
