Paid nominee voting and event ticketing platform for campus awards and live events (e.g. ASCES at UMaT). Paystack checkout, real-time leaderboards, nominee self-service dashboards, and admin tooling.
Live: voteeq.online · API: api.voteeq.online
flowchart LR
Web[React SPA Vercel]
API[Express API]
WS[WebSocket Leaderboard]
DB[(Turso libSQL)]
Pay[Paystack]
Email[Resend]
Web --> API
Web --> WS
API --> DB
API --> Pay
Pay -->|webhook| API
API --> Email
| Flow | Behavior |
|---|---|
| Vote purchase | User selects nominee → Paystack hosted checkout → webhook marks vote paid → receipt email |
| Leaderboard | WebSocket broadcast on vote confirmation |
| Ticketing | Optional TICKETS_ENABLED module with QR scan at door |
| Nominee portal | Self-upload photo, share cards, OG image generation (Sharp) |
| Layer | Host | Tech |
|---|---|---|
| Frontend | Vercel | React + Vite |
| API | Render | Express + WebSocket |
| Database | Turso | libSQL |
cd backend
cp .env.example .env
# Edit .env with your Turso credentials (optional — falls back to local SQLite)
npm install
npm run devAPI runs at http://localhost:5000.
cd frontend
npm install
npm run devApp runs at http://localhost:5173. API URL defaults to http://localhost:5000.
| Variable | Required | Notes |
|---|---|---|
NODE_ENV |
Yes | production |
JWT_SECRET |
Yes | Auto-generated in render.yaml |
ADMIN_USERNAME |
Yes | Admin login username |
ADMIN_PASSWORD |
Yes | Admin login password |
TURSO_DATABASE_URL |
Yes | libsql://... from Turso dashboard |
TURSO_AUTH_TOKEN |
Yes | Turso auth token |
CORS_ORIGIN |
Yes | Comma-separated, e.g. https://voteeq.online,https://www.voteeq.online,https://voteeq.vercel.app |
FRONTEND_URL |
Yes | Primary site URL for Paystack callbacks, e.g. https://www.voteeq.online |
PAYSTACK_SECRET_KEY |
When live | sk_live_... or sk_test_... — backend only, never in git or Vercel |
In the Paystack dashboard → Settings → API Keys & Webhooks:
- Add webhook URL:
https://api.voteeq.online/api/payment/webhook - Subscribe to
charge.success - Use the secret key (
sk_...) asPAYSTACK_SECRET_KEYon Render, then redeploy
Callback vs webhook
| Setting | Required in Paystack menu? | What Voteeq does |
|---|---|---|
| Callback URL | No | Sent on every checkout in API code → https://www.voteeq.online/payment-status?token=... (uses FRONTEND_URL on Render). Redirects the payer back to your site after payment. |
| Webhook URL | Yes | Paystack POSTs charge.success to your API so votes/tickets/registrations are marked paid. Without this, money can succeed but records stay pending. |
The public key (pk_...) is not required — checkout uses Paystack’s hosted payment page.
Votes and tickets require a valid email. After Paystack confirms payment, Voteeq emails a receipt via Resend.
| Variable | Required | Notes |
|---|---|---|
RESEND_API_KEY |
For live email | Create at Resend → API Keys (can reuse a key from another project or create a new one for Voteeq) |
RESEND_FROM_EMAIL |
Recommended | Default: Voteeq <onboarding@resend.dev> (testing — only delivers to your Resend account email). For production, add and verify voteeq.online in Resend, then e.g. Voteeq <receipts@voteeq.online> |
Set both on Render alongside Paystack keys, then redeploy the API.
| Variable | Value |
|---|---|
VITE_API_URL |
https://api.voteeq.online |
VITE_WS_URL |
wss://api.voteeq.online |
GET /health — returns { status: "ok" } when the API and database are reachable.
Use when Render Free cold starts are unacceptable. Instance apex-server has Elastic IP 54.153.93.87.
- DNS (Namecheap):
api.voteeq.online→ A record54.153.93.87(remove Render CNAME). - On the EC2 instance (Ubuntu), run
deploy/ec2-bootstrap.shfrom the repo, or seedeploy/ec2.env.example. - SSL:
sudo certbot --nginx -d api.voteeq.online - Paystack webhook:
https://api.voteeq.online/api/payment/webhook
Optional: EventBridge + Lambda pinging /health every 10 minutes can reduce Render Free sleep but is unreliable; EC2 avoids the problem entirely.
- Mock payment endpoints and USSD auto-complete are disabled in production.
- Demo seed data (sample nominees/events) is development only.
- Payment status lookups require a
tokenquery param returned at checkout. - Ticket lookup requires a ticket code (
TIX-...) or payment reference + buyer email.
MIT · Author: iamroidev