A free civic platform helping Haitians in the diaspora understand and participate in Haiti's historic election.
Vwa (Creole for "voice") is a nonpartisan civic resource built for the Haitian diaspora. It helps users understand what is at stake in Haiti's 2025 election, check their eligibility to participate, navigate documentation requirements, and stay informed as the process develops.
The platform is bilingual (English and Haitian Creole) and designed for accessibility across devices and connection speeds.
- Framework — Next.js 14 App Router, React 18, TypeScript
- Styling — Tailwind CSS, Radix UI, shadcn/ui
- Content — Velite (MDX compilation), bilingual via
[lang]route segment - Database — Prisma ORM, MySQL (PlanetScale-compatible)
- Auth — NextAuth.js (admin routes only; no public login required)
- Email — Postmark (subscriber notifications)
- Payments — Stripe (one-time donations, optional)
- Analytics — Vercel Analytics with typed event abstraction
- Package manager — pnpm (required — see note below)
| Path | Description |
|---|---|
/[lang] |
Homepage — hero, stats, eligibility preview, content cards, email capture |
/[lang]/eligibility |
Multi-step eligibility checker |
/[lang]/eligibility/result |
Eligibility result + next steps |
/[lang]/why-this-election-matters |
Editorial context |
/[lang]/resources |
Resource index |
/[lang]/resources/documentation |
Documentation guide |
/[lang]/timeline |
Election timeline |
/[lang]/about |
About Vwa |
/[lang]/support |
Support / donate page |
/admin |
Protected admin area (NextAuth required) |
Supported languages: en (English), ht (Haitian Creole). Language is detected from the URL path, cookie, or Accept-Language header.
- Node.js 18+
- pnpm 8+ (
npm install -g pnpm) - A MySQL database (local or PlanetScale)
- Install dependencies with pnpm:
pnpm installImportant: This project must be run with pnpm, not npm. pnpm manages the node_modules structure using its virtual store. Running scripts with npm will cause module resolution failures in velite's config compilation on Windows.
- Copy
.env.exampleto.env.localand fill in required values:
cp .env.example .env.localRequired:
DATABASE_URL=
NEXTAUTH_SECRET=
NEXT_PUBLIC_APP_URL=
Optional (features degrade gracefully when absent):
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
POSTMARK_API_TOKEN=
POSTMARK_SIGN_IN_TEMPLATE=
POSTMARK_ACTIVATION_TEMPLATE=
STRIPE_API_KEY=
STRIPE_WEBHOOK_SECRET=
NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PLAN_ID=
- Run database migrations:
npx prisma migrate dev- Start the development server:
pnpm devThe app will be available at http://localhost:3000. It redirects to /en by default.
To get live MDX reloads while editing content files, run velite in watch mode in a second terminal:
pnpm dev:content| Command | Description |
|---|---|
pnpm dev |
Build content once, then start Next.js dev server |
pnpm dev:content |
Watch MDX content files and rebuild on change |
pnpm build |
Production build (velite + next build) |
pnpm start |
Start production server |
pnpm lint |
ESLint |
pnpm check:types |
TypeScript type check (no emit) |
pnpm preview |
Full production build and local preview |
app/
[lang]/
(marketing)/ # Public-facing pages
(docs)/ # Documentation layout
components/ # Shared UI components
content/
pages/ # MDX static pages
resources/ # MDX resource and explainer content
lib/ # Utilities (auth, analytics, db, utils)
prisma/
schema.prisma # Database schema
public/
images/ # Static images (hero, etc.)
types/ # Shared TypeScript types
velite.config.ts # Content collection definitions
All editorial content lives in content/ as MDX files. Velite compiles them into .velite/ (auto-generated, never edit directly). Next.js pages import from .velite/ — they never read MDX files at runtime.
| Collection | Directory | Used for |
|---|---|---|
allPages |
content/pages/ |
Static pages: about, privacy, terms, why-this-election-matters |
allResources |
content/resources/ |
Explainers and guides: documentation, voter registration, etc. |
Pages (content/pages/):
---
title: Why This Election Matters
description: The historical context behind Haiti's 2025 election.
---
Your content here...Resources (content/resources/):
---
title: Documentation Guide
description: What documents you may need to participate.
category: documents
---
Your content here...The category field is optional and can be used for filtering on the resources index.
Slugs are derived automatically from the file path:
content/pages/about.mdx→ slug/aboutcontent/resources/documentation.mdx→ slug/resources/documentation
One-off edit — just restart the dev server; velite runs at startup:
pnpm devActive content session — run velite in watch mode in a second terminal so edits hot-reload immediately:
# Terminal 1
pnpm dev
# Terminal 2
pnpm dev:contentSave an MDX file → velite recompiles → Next.js picks up the change automatically.
- Create
content/pages/your-slug.mdxwith the required frontmatter - Create the corresponding route at
app/[lang]/(marketing)/your-slug/page.tsx - Import and render the compiled body from
.velite/
.velite/— wiped and regenerated on every velite buildpublic/static/— velite copies linked assets here automatically
- Subscriber — email capture with source tracking
- Donation — one-time donation records (Stripe-backed, optional)
- Issue — civic issues (stub, for future editorial use)
- Person — candidate/figure profiles (stub)
- Institution — institutional profiles (stub)
- TimelineEntry — election timeline entries
MIT