Crafting experience...
3/6/2026
A Project Made By
Submitted for
Built At
HACKATHON - University of West London
Hosted By
🏛️
सरकार Digital
Nepal e-Government Platform
TECHNICAL DOCUMENTATION
Version 1.0 | BCS Hackathon 2025
Built for: BCS – Build Something Secure From Day One
1. Executive Summary What is Sarkar Digital? |
Sarkar Digital (सरकार Digital) is a full-stack, secure-by-design digital government platform built for Nepal. It provides citizens with a single, authenticated portal to access government services, participate in democratic elections, report corruption anonymously, and engage in civic discourse — all underpinned by a robust security architecture designed to protect both citizen data and the integrity of democratic processes.
This system was developed as the submission for the BCS – Build Something Secure From Day One hackathon, sponsored by the BCS, Chartered Institute for IT. Every architectural decision was made with security as a primary concern, not an afterthought.
Feature | Description |
Platform | Sarkar Digital — Nepal e-Government Portal |
Backend | Python 3 · Flask · SQLAlchemy · Flask-JWT-Extended |
Frontend | Vanilla HTML/CSS/JS (no framework dependencies) |
Database | SQLite (development) — drop-in PostgreSQL ready |
Authentication | JWT access tokens (30 min) + refresh tokens (24 hr) |
Password hashing | bcrypt with 12 cost rounds |
Key security | Secure-by-design: 10 implemented controls mapped to real threats |
Citizen portal | |
Admin panel |
2. System Architecture How everything fits together |
The system follows a monolithic Flask application pattern with a clean blueprint-based modular structure. The frontend is served directly by Flask (no CORS issues in production), and all API endpoints live under /api/*. The single database is accessed exclusively through SQLAlchemy's ORM — no raw SQL strings, eliminating SQL injection by construction.
sarkar/
├── app.py # Application factory, blueprint registration, seed
├── config.py # Environment-based configuration
├── database.py # SQLAlchemy instance
├── models.py # All ORM models
├── requirements.txt
├── .env.example
├── templates/
│ ├── index.html # Citizen portal (SPA)
│ └── admin.html # Admin panel (SPA)
└── routes/
├── auth.py # Login, refresh token
├── citizen.py # Registration, /api/me
├── voting.py # Elections, casting votes, results
├── services.py # Government services, applications
├── whistleblower.py # Anonymous report submission & tracking
├── community.py # Citizen posts, likes
└── admin.py # Full admin API (stats, approvals, CRUD)
The following entities form the core data layer:
Feature | Description |
Citizen | Core user entity. Stores personal details, hashed password, district, approval status (pending/approved/rejected), role (citizen/officer/admin), and optional position assignment. |
Role | Named roles with a JSON permissions array. Decoupled from the built-in citizen.role field — allows fine-grained permission definitions for government officers. |
GovernmentPosition | Titles like 'Ward Chair — Ward 10'. Linked to a Role and optionally assigned to a Citizen holder. Has level (ward → federal) and location fields. |
Election | Supports three voting modes: online, paper, hybrid. Has district field which gates voter eligibility. Cascades delete to Candidates and Votes. |
Candidate | Belongs to an Election. Can be added manually or imported from the CSV CandidatePool. Stores paper_votes count for paper/hybrid elections. |
Vote | Immutable online vote record. DB-level unique constraint on (citizen_id, election_id) prevents double-voting even under concurrent requests. |
CandidatePool | 50,000 records imported from CSV. Used as a source to populate election candidates without touching the live Candidate table until explicitly added. |
Report | Fully anonymous. No citizen_id ever stored. 64-char hex case token is the only link. Supports admin status updates and notes. |
Service | Government service definition (e.g. Passport Application). |
ServiceApplication | Citizen application for a Service. Tracks status through Pending → Processing → Approved/Rejected lifecycle. |
CommunityPost | Citizen-authored posts with category, like count, pinned and hidden flags controlled by admin. |
1. Browser sends request to Flask server (single process, port 5000)
2. For / and /admin: Flask renders the template directly
3. For /api/*: Blueprint route handler runs
4. JWT middleware validates Authorization: Bearer <token> header
5. Handler queries DB via SQLAlchemy ORM (parameterised — no raw SQL)
6. JSON response returned; tokens refreshed by frontend before expiry
3. Feature Reference Complete feature documentation |
Citizens register by filling in their citizenship certificate details: full name in Nepali and English, date of birth, gender, province, district, municipality, ward number, citizenship number, issuing district, parents' names, mobile, and an email address (optional). A password of at least 8 characters is required.
Upon submission, the account is created with status = pending. The citizen cannot vote, apply for services, or make community posts until an administrator approves the account. This mirrors the real-world process of identity verification by a government officer.
• Security note: Passwords are hashed using bcrypt (12 rounds) before storage. The plaintext password is never written to the database or logged.
Citizens log in with their citizenship number and password. On success, the API returns a short-lived JWT access token (30 minutes) and a long-lived refresh token (24 hours). Both are stored in JavaScript memory — never in localStorage or cookies — so they are cleared on browser close.
• Pending accounts: Can log in and browse the portal, but all protected actions (vote, apply, post) are blocked with a clear 'account pending approval' message and warning banner.
Eight pre-seeded services are available:
• Passport Application — Department of Passports
• Citizenship Certificate — District Administration Office
• Driving License — Department of Transport Management
• Birth Certificate — Municipal Office
• Land Registration — Land Revenue Office
• Tax Filing — Inland Revenue Department
• Loksewa Exam Registration — Public Service Commission
• Business Registration — Office of Company Registrar
Approved citizens click a service card to apply. The backend prevents duplicate pending applications for the same service. Citizens track their applications (Pending / Processing / Approved / Rejected) on their personal dashboard.
The elections page lists all elections with their status badge (Upcoming / Open / Closed) and voting mode badge (Online / Paper / Hybrid). Selecting an election shows the full candidate list with party, age, gender, and district information.
Voting rules enforced by the backend:
• Account must be approved (status = approved)
• Election must be open (status = open)
• Voting mode must not be paper-only
• District eligibility: If the election has a district field set, the citizen's registered district must match. This enforces geographic electoral boundaries automatically.
• One vote per citizen per election (enforced by DB unique constraint — race-condition safe)
Once voted, the candidate card shows a green 'Your Vote' confirmation badge. The vote button disappears for all other candidates.
An open civic forum where approved citizens can post ideas, feedback, or complaints. Posts are categorised (General / Idea / Feedback / Complaint) and can be liked by any logged-in citizen. Pinned posts (set by admin) always appear at the top. Admin-hidden posts are invisible to citizens.
Submission: Completely anonymous. Citizens select a category (Corruption, Bribery, Misuse of Authority, Election Fraud, Financial Fraud, Human Rights Violation, Police Misconduct, Other), a level (Ward → Federal), write a description of at least 20 characters, and optionally attach a file. No citizen_id, session, or IP address is stored.
Case Token: The system generates a 64-character cryptographically random hex token (secrets.token_hex(32)). This is the only identifier for the report. The citizen must save it to track their case — there is no recovery mechanism, by design.
Tracking: Entering the case token shows the report status (Received / Investigating / Resolved) and any public admin notes.
File security: Uploaded files are extension-whitelisted (pdf, png, jpg, jpeg, docx, txt), and the filename is replaced with a 16-character SHA-256-derived hex name to prevent metadata leakage and path traversal.
After login, citizens see a personal dashboard with:
• Profile summary (name, citizenship number, district, role, approval status, position if assigned)
• Pending approval warning banner for unverified accounts
• Service application history with live status badges
• Voting status across all open elections — shows candidate voted for, or a link to vote
All admin routes require a valid JWT token where citizen.role = 'admin'. The admin panel is a separate single-page application served at /admin, with its own login form. Every API call from the admin panel is independently re-validated server-side — the frontend role check is defence-in-depth only.
Real-time statistics: total citizens, pending approvals, total elections (with count of currently open), new whistleblower reports, pending service applications, and candidate pool size. The two quick-action widgets show the pending approval queue and newest unread reports directly on the landing screen.
A searchable, paginated table of all citizens filterable by approval status (All / Pending / Approved / Rejected). For each citizen the admin can:
• Approve registration → sets status = approved, citizen can now use full platform
• Reject registration → sets status = rejected with an optional reason (shown to citizen on login)
• Change role → citizen / officer / admin
• Assign a government position → links the citizen to a GovernmentPosition record
• View full profile including DOB, parents' names, municipality, and ward
Admins create named roles (e.g. 'District Officer', 'Election Commissioner') with a free-text description and a JSON array of permission strings (e.g. ["approve_citizens", "manage_services"]). Roles are assigned to GovernmentPositions, not directly to citizens — a citizen's system role (citizen/officer/admin) is separate.
Positions represent real government roles: title, department, administrative level (Ward / Municipal / District / Provincial / Federal), and geographic location (province, district, municipality). A position can be linked to a Role and assigned to a Citizen holder. The positions table shows whether each position is vacant or occupied.
The election management screen lets admins create and edit elections with full control over:
Feature | Description |
Name & Type | Descriptive name and type (Local / Provincial / Federal) |
Voting Mode | Online Only — citizens vote via the portal Paper Only — voting at polling stations, admin enters results later Hybrid — both channels run simultaneously; results are compared for discrepancy detection |
District | If set, only citizens registered in this district can cast online votes. Leave blank for national elections. |
Status | Upcoming / Open / Closed — controls whether voting is active |
Registered Voters | Total registered voter count for paper/hybrid elections (used in turnout calculations) |
Dates | Optional start and end datetime for the election window |
Each election has a manage screen with two ways to add candidates:
• Manual entry: Enter name, party, age, gender, district, and electoral symbol individually.
• Import from pool: Filter the 50,000-record CSV pool by party, province, and district, then import up to 100 at a time. Already-imported candidates (by CSV ID) are skipped to prevent duplicates.
For paper or hybrid elections, the manage screen shows a form to enter paper vote counts per candidate. In hybrid mode, once both online and paper results are entered:
• The results endpoint calculates online votes + paper votes = total
• A discrepancy value is computed (|online_total - paper_total|)
• If discrepancy > 100, a prominent warning is shown on the public results page
• Paper results take precedence in the final winner determination
The admin can import the bundled nepal_synthetic_candidate_dataset_50k.csv (50,000 records) into the CandidatePool table in one click. The import uses batch inserts (1,000 rows at a time) for performance. The pool can be filtered and browsed by party, province, and district, with pagination. Pool data is used as a source for populating election candidate lists.
All reports are listed with their category, level, status badge, submission date, and a file attachment indicator. The admin opens a report to read the full description and update its status (Received → Investigating → Resolved). Admin notes are stored and shown to the reporter when they track by token.
Paginated list of all service applications, filterable by status. The admin opens an application to update its status and add notes (e.g. 'Document verification pending', 'Approved — certificate dispatched').
All community posts are shown in a table. Admins can pin a post (it will appear at the top of the public feed), hide a post (invisible to citizens), or permanently delete it. The author name and district are shown to assist with moderation decisions.
4. API Reference All endpoints, methods, and authentication |
All protected endpoints require a JWT Bearer token in the Authorization header:
Authorization: Bearer <access_token>
Tokens are obtained via POST /api/login. Access tokens expire after 30 minutes; use POST /api/refresh with the refresh token to obtain a new access token without re-entering credentials.
Method | Endpoint | Auth | Description |
POST | /api/login | Public | Login with citizenship_number + password. Returns access_token, refresh_token, citizen info. |
POST | /api/refresh | JWT Refresh | Exchange refresh token for new access token. |
Method | Endpoint | Auth | Description |
POST | /api/register | Public | Register new citizen. Returns pending status. All fields from citizenship certificate required. |
GET | /api/me | JWT | Get own profile including approval status, role, position, and district. |
Method | Endpoint | Auth | Description |
GET | /api/elections | Public | List all elections with candidate count, online vote count, and mode. |
GET | /api/elections/:id | Public | Get election detail with full candidate list. Shows votes only if status=closed. |
GET | /api/elections/:id/my-vote | JWT | Check if authenticated citizen has voted in this election. |
POST | /api/vote | JWT | Cast a vote. Enforces district eligibility, approval status, and deduplication. |
GET | /api/elections/:id/results | Public | Get results. Only available when status=closed. Includes discrepancy for hybrid. |
Method | Endpoint | Auth | Description |
GET | /api/services | Public | List all active government services. |
POST | /api/services/apply | JWT | Submit service application. Requires approved account. |
GET | /api/services/my-applications | JWT | Get own application history with status. |
PUT | /api/services/applications/:id | JWT Officer/Admin | Update application status and notes. |
Method | Endpoint | Auth | Description |
POST | /api/report | Public | Submit anonymous report (multipart/form-data). Returns 64-char case token. |
GET | /api/report/track | Public | Track report by ?token=<64-char-token>. Returns status and public admin notes. |
Method | Endpoint | Auth | Description |
GET | /api/community | Public | List visible, non-hidden posts. Filter by ?category=. Paginated. |
POST | /api/community | JWT (Approved) | Create new post. Requires approved account. |
POST | /api/community/:id/like | JWT | Increment like count on a post. |
DELETE | /api/community/:id | JWT | Delete own post. Admin/officer can delete any post. |
All admin endpoints require JWT where citizen.role = 'admin'.
Method | Endpoint | Auth | Description |
GET | /api/admin/stats | Admin | Dashboard stats: citizens, elections, reports, services, pool. |
GET | /api/admin/citizens | Admin | Paginated citizen list. Filter by status, search by name/CN/district. |
GET | /api/admin/citizens/:id | Admin | Full citizen profile. |
POST | /api/admin/citizens/:id/approve | Admin | Approve citizen registration. |
POST | /api/admin/citizens/:id/reject | Admin | Reject with optional reason. |
POST | /api/admin/citizens/:id/role | Admin | Set role: citizen / officer / admin. |
POST | /api/admin/citizens/:id/position | Admin | Assign or remove government position. |
GET | /api/admin/roles | Admin | List all roles. |
POST | /api/admin/roles | Admin | Create role with permissions JSON. |
PUT | /api/admin/roles/:id | Admin | Update role. |
DELETE | /api/admin/roles/:id | Admin | Delete role. |
GET | /api/admin/positions | Admin | List positions with holder info. |
POST | /api/admin/positions | Admin | Create position. |
DELETE | /api/admin/positions/:id | Admin | Delete position (unlinks holder). |
POST | /api/admin/elections | Admin | Create election. |
PUT | /api/admin/elections/:id | Admin | Update election (mode, status, dates, etc.). |
DELETE | /api/admin/elections/:id | Admin | Delete election + all candidates + votes. |
POST | /api/admin/elections/:id/candidates | Admin | Manually add candidate. |
DELETE | /api/admin/elections/:id/candidates/:cid | Admin | Remove candidate. |
POST | /api/admin/elections/:id/candidates/import-csv | Admin | Bulk import from CandidatePool with filters. |
POST | /api/admin/elections/:id/paper-results | Admin | Enter paper ballot vote counts per candidate. |
POST | /api/admin/candidate-pool/import | Admin | Import nepal_synthetic_candidate_dataset_50k.csv into DB. |
GET | /api/admin/candidate-pool | Admin | Browse pool with search/filter/pagination. |
GET | /api/admin/candidate-pool/parties | Admin | Party list with candidate counts. |
GET | /api/admin/reports | Admin | Paginated reports, filter by status. |
GET | /api/admin/reports/:id | Admin | Full report detail including description. |
PUT | /api/admin/reports/:id | Admin | Update report status and admin notes. |
GET | /api/admin/service-applications | Admin | All service applications, paginated. |
PUT | /api/admin/service-applications/:id | Admin | Update application status. |
GET | /api/admin/community | Admin | All community posts (including hidden). |
POST | /api/admin/community/:id/pin | Admin | Toggle pin status. |
POST | /api/admin/community/:id/hide | Admin | Toggle hidden status. |
DELETE | /api/admin/community/:id | Admin | Permanently delete post. |
5. Security Architecture Secure by design — controls, threats, and mitigations |
Ten security controls are implemented. Each was designed into the system from the start, not added retrospectively.
What: All passwords are hashed using bcrypt with a cost factor of 12 before storage.
Why: bcrypt is adaptive — the cost factor can be increased as hardware improves. At 12 rounds, a single hash takes ~250ms, making offline brute-force attacks economically infeasible.
Where: routes/citizen.py:register(), routes/auth.py:login()
hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(rounds=12))
What: Stateless JWT tokens with short expiry (30 min access, 24 hr refresh). All sensitive state is server-side in the DB.
Why: Short-lived tokens limit the damage window if a token is intercepted. The refresh mechanism means legitimate users aren't constantly asked to re-authenticate.
Where: routes/auth.py, config.py (JWT_ACCESS_TOKEN_EXPIRES = 30 minutes)
What: Three platform roles (citizen / officer / admin). Every admin endpoint independently re-validates the requester's role from the DB — the JWT contains the user's ID, not their role, so a compromised token cannot escalate privileges by modifying its payload.
Why: Least-privilege principle. Citizens cannot access admin functions; officers cannot delete elections.
What: Citizens are created with status = pending. They cannot vote, apply for services, or post to the community board until an admin explicitly approves their identity.
Why: Prevents fake accounts from voting in elections or flooding government service queues. Mirrors real-world identity verification.
What: If an election has a district field set, the voter's registered district (from the DB, not a user-supplied parameter) must match before a vote is accepted.
Why: Prevents cross-constituency voting fraud. The eligibility check uses data from the server-side database, not anything the client sends.
if election.district and citizen.district.lower() != election.district.lower():
return jsonify({'error': 'Not eligible for this election'}), 403
What: A database-level UNIQUE constraint on (citizen_id, election_id) in the votes table. This is enforced at the DB layer, not just application code.
Why: Application-level checks can fail under concurrent requests (TOCTOU race condition). The DB constraint is atomic and cannot be bypassed.
__table_args__ = (db.UniqueConstraint('citizen_id', 'election_id', name='unique_citizen_election_vote'),)
What: Reports store no user identity. The case token is generated with secrets.token_hex(32) (256 bits of entropy). No session cookie or IP address is logged.
Why: Legal and ethical requirement: a whistleblower who fears retaliation must be completely untraceable. Even a database breach cannot link a report to its author.
What: Uploaded filenames are discarded. Extensions are whitelisted against a known-safe set. The file is saved with a 16-char SHA-256-derived hex name.
Why: Prevents directory traversal (../../../etc/passwd), prevents execution of server-side scripts uploaded as 'images', and prevents metadata leakage from filenames.
ALLOWED_EXTENSIONS = {'pdf', 'png', 'jpg', 'jpeg', 'docx', 'txt'}
hashed_name = hashlib.sha256(os.urandom(32)).hexdigest()[:16] + f'.{ext}'
What: Every database interaction uses SQLAlchemy's ORM. There are zero raw SQL strings with user-supplied values anywhere in the codebase.
Why: The ORM produces parameterised queries automatically. User input is never concatenated into SQL.
# Safe — ORM generates parameterised query:
Citizen.query.filter_by(citizenship_number=cn).first()
What: All API endpoints validate required fields, minimum lengths (password ≥ 8, report description ≥ 20 chars), valid values (role must be citizen/officer/admin), and entity ownership before making any DB changes.
Why: Defence in depth. Even if other controls fail, malformed data is rejected before it reaches the database.
6. Threat Modelling Assets, attack surfaces, and residual risks |
• Citizen identity data (name, DOB, citizenship number, address, biometric-adjacent data)
• Authentication credentials (passwords — stored as bcrypt hashes only)
• Election vote integrity (each citizen's vote must be counted once and accurately)
• Whistleblower anonymity (report contents must not be linkable to the reporter)
• Government service application records
• JWT signing secrets (SECRET_KEY, JWT_SECRET_KEY)
• HTTP API endpoints (public internet, no authentication on public routes)
• Authentication flow (login brute-force, token theft, credential stuffing)
• File upload endpoint (malicious file upload, path traversal)
• Database (SQL injection, direct access if server is compromised)
• JWT tokens (forgery if secret leaks, replay if not expired)
• Admin panel (privilege escalation, unauthorised access)
• Voting system (double-vote, eligibility bypass, result manipulation)
Threat | Severity | Attack Surface | Mitigation | Residual Risk |
Credential Brute-Force | High | POST /api/login | bcrypt 12 rounds adds ~250ms per attempt; rate limiting recommended in production | Low |
SQL Injection | Critical | All /api/* endpoints | SQLAlchemy ORM — all queries parameterised; no raw SQL with user input | Low |
JWT Token Theft / Replay | High | All authenticated requests | 30-min access token TTL; HTTPS required in production; no storage in localStorage | Low |
Double Voting | Critical | POST /api/vote | DB-level UNIQUE constraint — atomic, race-condition safe | Low |
Cross-Constituency Voting | High | POST /api/vote | District field compared server-side from DB (not user input) | Low |
Fake Account Flooding | Medium | POST /api/register | Admin approval gate — all accounts start as 'pending' | Low |
Whistleblower De-Anonymisation | Critical | POST /api/report | No citizen_id stored; case token is 256-bit random; IP not logged | Low |
Malicious File Upload | High | POST /api/report (file) | Extension whitelist; filename discarded and replaced with SHA-256 hex | Low |
Privilege Escalation | Critical | All /api/admin/* endpoints | Every admin endpoint re-fetches citizen.role from DB; JWT payload not trusted for role | Low |
Path Traversal | High | File upload / download | os.path.basename() + filename discard; upload directory isolated | Low |
IDOR (Insecure Direct Object Reference) | High | GET /api/citizens/:id | Ownership check: can only view own profile unless role=admin | Low |
Hybrid Election Result Tampering | High | POST /api/admin/elections/:id/paper-results | Admin-only endpoint; paper results vs online votes discrepancy detection exposes manipulation | Medium |
The following risks remain at medium level and are acknowledged:
• Hybrid election discrepancy (Medium): Paper ballot stuffing by a malicious election officer could go undetected if they also control the online entry form. Mitigation in production: multi-party result entry and independent audit trail.
• No rate limiting (Medium): The current implementation does not include API rate limiting. In production, a reverse proxy (nginx) or middleware should limit POST /api/login to ~5 requests/minute per IP.
• SQLite in development (Low): SQLite has no network interface and is not accessible remotely, but lacks the access controls of PostgreSQL. Production deployment should use PostgreSQL with a dedicated DB user.
• No HTTPS enforcement (Low): The Flask dev server runs HTTP. Production must terminate TLS at a reverse proxy (nginx + Let's Encrypt). Without HTTPS, JWT tokens could be intercepted on the wire.
7. Setup & Deployment Running the platform locally |
• Python 3.10+ with pip
• Node.js (optional — only needed if regenerating documentation)
• The nepal_synthetic_candidate_dataset_50k.csv file in the project root
# 1. Clone / extract the project
cd sarkar
# 2. Create and activate virtual environment
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# 3. Install dependencies
pip install -r requirements.txt
# 4. Configure environment
cp .env.example .env
# Edit .env and set strong SECRET_KEY and JWT_SECRET_KEY
# 5. Run
python app.py
# Citizen portal: http://localhost:5000
# Admin panel: http://localhost:5000/admin
A system administrator account is seeded on first run:
Feature | Description |
Citizenship Number | ADMIN-001 |
Password | admin1234 |
Role | admin |
Status | approved |
⚠ Change the admin password immediately after the first login in any non-demo environment.
7. Open http://localhost:5000/admin → login as ADMIN-001 / admin1234
8. Go to Elections → Create a new election (e.g. Ward 10 Local Election, mode: Online, district: Kathmandu, status: Open)
9. Manage the election → Add a few candidates manually, or import from pool after importing CSV
10. Open http://localhost:5000 in a new tab → Register a citizen (use district: Kathmandu)
11. Back in admin → Citizens → approve the new registration
12. Back in citizen portal → login → go to Elections → cast a vote
13. In admin → change election status to Closed → view results
14. In citizen portal → submit a whistleblower report → copy the case token → track it
15. In admin → Whistleblower → update the report status to Investigating
16. In citizen portal → track again → see updated status
Feature | Description |
Flask | Web framework and routing |
Flask-SQLAlchemy | ORM — database abstraction and migration |
Flask-JWT-Extended | JWT token creation, validation, and refresh |
Flask-CORS | Cross-origin resource sharing (API access from browser) |
bcrypt | Password hashing with adaptive cost factor |
8. Design Decisions Why we chose what we chose |
Flask's micro-framework nature gives us complete control over every component. For a hackathon submission focused on security, this means we can explicitly show every authentication check, every validation, and every security control in the code without framework magic obscuring the implementation. It also keeps the dependency surface minimal.
Storing JWTs in httpOnly cookies would protect against XSS but introduces CSRF risk. Storing in localStorage introduces XSS risk. We chose in-memory storage (JavaScript variable): tokens are never persisted to disk, are cleared on browser close, and are not accessible to third-party scripts. The tradeoff is that users must log in again after closing the tab — an acceptable UX cost for a government security platform.
The voter eligibility check reads citizen.district directly from the database, not from the JWT. If district were in the JWT, a user could potentially forge or replay an old token from a different address. By always re-reading from the DB, we ensure the check reflects the current authoritative record.
An application-level check (query for existing vote, then insert) has a TOCTOU (time-of-check/time-of-use) race condition: two simultaneous requests could both pass the check and both insert. A database UNIQUE constraint is an atomic, single operation that the DB guarantees regardless of concurrency — a much stronger guarantee.
By design, there is no token recovery mechanism. If the whistleblower loses their token, the report cannot be tracked by them — but it also cannot be linked to them by anyone else, including a database administrator. The security guarantee (complete anonymity) is more important than the convenience (recovery). This is the correct tradeoff for a corruption reporting system.
Nepal's electoral system currently uses paper ballots as the authoritative record. A purely online system would be a step change that requires complete infrastructure confidence. The hybrid mode allows a gradual transition: run both systems simultaneously, compare results for discrepancies, and if they match, build confidence in online-only. If they don't match, paper takes precedence. This is a realistic and responsible design.
9. Legal & Ethical Considerations Privacy, data protection, and responsible design |
The system collects only the data strictly necessary for each function. The whistleblower system collects no personal data at all. Service applications do not require uploading physical documents. Community posts do not require a verified identity beyond being an approved citizen.
• Passwords are never stored in plaintext or recoverable form
• Citizenship numbers are stored but never logged to application logs
• Whistleblower reports contain no identifying information by design
• The admin panel is protected behind authentication — citizens cannot see each other's data
• Votes are immutable once cast — no UPDATE or DELETE on the votes table is permitted from application code
• Results are not shown until the election is closed — preventing vote-buying or strategic last-minute voting
• The hybrid discrepancy check provides an audit mechanism against ballot stuffing
• District eligibility is checked server-side against authoritative DB data
This documentation explicitly identifies known residual risks (Section 6) rather than presenting the system as fully secure. A responsible security posture acknowledges limitations and provides a path to addressing them in production deployment.
• Right to be informed: citizens see their approval status and any rejection reason
• Data accuracy: citizens' district and personal data are recorded from official citizenship documents
• Data minimisation: anonymous reporting collects no personal data
• Storage limitation: production deployment should implement data retention policies
10. Future Roadmap What comes next |
• Replace SQLite with PostgreSQL for production
• Add nginx reverse proxy with HTTPS (Let's Encrypt)
• Add API rate limiting on /api/login (5 req/min per IP)
• Add two-factor authentication (TOTP) for admin accounts
• Implement structured audit logging for all admin actions
• Add CSRF token to sensitive form submissions
• Officer dashboard — allow officers to approve service applications without full admin access
• Notification system — SMS/email alerts on application status change
• Document upload for service applications (with the same file security controls as whistleblower)
• Election result charts — visual breakdown of results with vote share percentages
• Multi-language support — Nepali (Devanagari) UI strings throughout
• Blockchain-anchored vote receipts — cryptographic proof that a vote was counted
• Integration with Nepal's National Identity Card database for automated identity verification
• Biometric authentication option for high-security operations
• Federated login across provincial government portals
• Public API for civil society organisations to access anonymised election statistics
Sarkar Digital · BCS Hackathon 2025 · Built Secure From Day One