A secure, anonymous voting platform enabling organizations to host democratic elections with triple-layer fraud prevention, real-time results, and automated Excel reporting.
NOTA (None Of The Above) is a modern, production-ready online voting application that enables organizations, student councils, educational institutions, and community groups to conduct secure, transparent elections. Built with enterprise-grade security and privacy features, NOTA ensures fair democratic processes through multi-layer fraud prevention.
✅ Firebase Email/Password Authentication: Secure login system using Firebase Auth
✅ Neon PostgreSQL Database: Serverless Postgres for storing elections and votes
✅ Private Election Links: Share unique URLs with voters
✅ Duplicate Vote Prevention: Email-based hashing prevents multiple votes
✅ Excel Export: Download voting results as Excel files
✅ Responsive UI: Clean, minimal interface built with Tailwind CSS
Before you begin, ensure you have:
- Node.js (v18 or higher)
- npm or yarn
- Firebase Project with Email/Password Authentication enabled
- Neon Database account and connection string
git clone <your-repo-url>
cd notanpm installThis will install all required packages:
firebase- Firebase SDK for authenticationpg- PostgreSQL client for Neonuuid- Generate unique election IDsxlsx- Excel file generationcookie,js-cookie- Cookie handlingtailwindcss,postcss,autoprefixer- Styling
- Go to Firebase Console
- Create a new project or use an existing one
- Enable Email/Password Authentication:
- Go to Authentication → Sign-in method
- Enable Email/Password provider
- Save changes
- Get your Firebase config:
- Go to Project Settings → General
- Scroll to "Your apps" and copy the config values
- Go to Neon Console
- Create a new project
- Copy the Connection String (DATABASE_URL)
- Make sure to include
?sslmode=requireat the end
Create a .env.local file in the root directory:
# Firebase Configuration
NEXT_PUBLIC_FIREBASE_API_KEY=your_firebase_api_key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your_project_id.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your_project_id
NEXT_PUBLIC_FIREBASE_APP_ID=your_firebase_app_id
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your_sender_id
# Neon Database Configuration
DATABASE_URL=postgresql://user:password@host/database?sslmode=require
# Application Base URL
NEXT_PUBLIC_APP_BASE_URL=http://localhost:3000.env.local to GitHub! It's already in .gitignore.
Run the SQL commands from schema.sql in your Neon database console:
CREATE TABLE elections (
id text PRIMARY KEY,
title text,
data jsonb,
closed boolean default false,
created_by text,
created_at timestamptz default now()
);
CREATE TABLE votes (
id serial PRIMARY KEY,
election_id text REFERENCES elections(id),
email_hash text,
vote_json jsonb,
created_at timestamptz default now(),
UNIQUE(election_id, email_hash)
);
CREATE INDEX idx_votes_election_id ON votes(election_id);
CREATE INDEX idx_elections_created_by ON elections(created_by);
CREATE INDEX idx_votes_email_hash ON votes(election_id, email_hash);You can run this in the Neon SQL Editor, or the database will be automatically set up on first use.
npm run devOpen http://localhost:3000 in your browser.
/nota
├─ pages/
│ ├─ _app.tsx # App wrapper with global styles
│ ├─ index.tsx # Home page (landing)
│ ├─ login.tsx # Email/Password login/signup page
│ ├─ host.tsx # Create election page
│ ├─ vote.tsx # Paste election link page
│ ├─ my-elections.tsx # View hosted elections
│ ├─ election/[id].tsx # Voting page for specific election
│ └─ api/
│ ├─ createElection.ts # API: Create new election
│ ├─ submitVote.ts # API: Submit vote (with email hash)
│ ├─ getElection.ts # API: Get election details
│ └─ downloadResults.ts # API: Download results as Excel
├─ lib/
│ ├─ firebase.ts # Firebase client configuration
│ └─ db.ts # Neon Postgres connection pool
├─ components/
│ ├─ Layout.tsx # Page layout wrapper
│ └─ FormField.tsx # Form field component
├─ styles/
│ └─ globals.css # Global styles with Tailwind
├─ schema.sql # Database schema (email_hash based)
├─ .env.local # Environment variables (gitignored)
├─ .gitignore # Git ignore file (protects secrets)
└─ package.json
-
Sign Up / Login:
- Go to
/login - Create an account with email and password (min 6 characters)
- Or login if you already have an account
- Go to
-
Create an Election:
- Click "Host an election"
- Enter election title
- Add positions and candidates
- Click "Create Election" to generate a shareable link
-
Share the Link:
- Copy the generated election link
- Share it with voters via WhatsApp, email, etc.
- Voters must be logged in to vote
-
View Results:
- Go to "My Elections"
- Click on your election
- View vote counts
- Download results as Excel
-
Login:
- Open the election link shared by the host
- If not logged in, click "Go to Login"
- Sign up or login with your email
-
Vote:
- Select your candidates for each position
- Click "Submit Vote"
- Vote is submitted anonymously (only email hash is stored)
-
Duplicate Prevention:
- Each email can only vote once per election
- If you try voting again, you'll see an error message
Creates a new election in the database.
Request Body:
{
"id": "uuid-string",
"title": "Student Council Election",
"positions": [
{
"name": "President",
"candidates": ["Alice", "Bob", "NOTA"]
}
]
}Response:
{
"ok": true
}Submits a vote for an election.
Request Body:
{
"id": "election-uuid",
"votes": {
"0": 1,
"1": 0
}
}Response:
{
"ok": true
}Fetches election details (title and positions).
Response:
{
"election": {
"title": "Student Council Election",
"positions": [...]
}
}Closes an election and returns all votes.
Request Body:
{
"id": "election-uuid"
}Response:
{
"ok": true,
"votes": [
{"0": 1, "1": 0},
{"0": 2, "1": 1}
]
}git init
git add .
git commit -m "Initial commit"
git remote add origin <your-github-repo>
git push -u origin main- Go to Vercel Dashboard
- Click "New Project"
- Import your GitHub repository
- Configure environment variables:
- Add all variables from
.env.local - Make sure
NEXT_PUBLIC_APP_BASE_URLis set to your production URL (e.g.,https://nota.vercel.app)
- Add all variables from
- Click "Deploy"
- Add your Vercel domain to Firebase authorized domains:
- Firebase Console → Authentication → Settings → Authorized domains
- Add your Vercel URL (e.g.,
nota.vercel.app)
-
Email-Based Duplicate Prevention:
- Votes are stored with SHA-256 hashed emails
- Each email can only vote once per election (enforced by database UNIQUE constraint)
- Emails are never stored in plain text for voter privacy
-
Environment Variables:
- Never commit
.env.localto version control - All sensitive credentials (Firebase, Neon) must be kept secret
- Use Vercel environment variables for production deployment
- Never commit
-
Firebase Authentication:
- Email/Password auth is free (no billing required)
- For production, consider enabling email verification
- Add password reset functionality for better UX
-
Rate Limiting:
- Add rate limiting to API routes to prevent abuse
- Consider using Vercel Edge Config or upstash/redis for tracking
-
Input Validation:
- Add proper input validation and sanitization
- Use libraries like
zodorjoifor schema validation - Sanitize user inputs before storing in database
-
Database Security:
- Neon connection string must be kept secret
- SSL mode is required for secure connections
- Consider using connection pooling limits in production
-
CORS & Security Headers:
- Configure proper CORS policies in production
- Add security headers in
next.config.ts:- Content-Security-Policy
- X-Frame-Options
- X-Content-Type-Options
- Make sure Email/Password is enabled in Firebase Console
- Check browser console for authentication errors
- Verify
.env.localhas correct Firebase credentials
- Verify
DATABASE_URLis correct and includes?sslmode=require - Check Neon project is active (not in sleep mode)
- Ensure database schema is created (run
schema.sql) - Check connection limits in Neon dashboard
- Make sure
postcss.config.mjsandtailwind.config.tsexist - Restart dev server after config changes
- Check
globals.cssimports Tailwind directives - Clear
.nextcache:rm -rf .nextand restart
- Ensure database tables are created with
email_hashcolumn (notphone_hash) - Verify user is logged in before voting
- Check browser console and terminal for error details
- Framework: Next.js 16.0.3 (Pages Router)
- Language: TypeScript 5
- Authentication: Firebase Auth (Email/Password)
- Database: Neon (Serverless PostgreSQL)
- Styling: Tailwind CSS v3.4.17
- Export: SheetJS (xlsx)
- Build Tool: Turbopack