Self-Hosting Guide
PlacePrep can be run entirely on your own infrastructure — on a single machine, a college server, or a LAN setup for offline exams. This guide covers everything you need to deploy it yourself.
For a quick reference of every important command, see the Technical Reference page.
Requirements
- Docker Desktop (Windows/Mac) or Docker Engine (Linux) — v24+
- Bun v1.x — for running scripts and development mode
- 10 GB+ free disk space — for Docker images and Appwrite data
- Appwrite Cloud account (free tier works) — used as the production database
- Node.js is not required — Bun handles everything
Architecture
The self-hosted exam mode runs two separate environments:
| Environment | Used for | Database |
|---|---|---|
| Cloud (Appwrite Cloud) | Normal usage, production data | Cloud Appwrite |
| Local (Docker on your machine) | Offline exams on LAN | Local Appwrite |
The two environments use the same schema and same user IDs, so data syncs cleanly between them.
Sync directions:
| Direction | When | What moves |
|---|---|---|
| Cloud → Local (Pre-exam) | Before the exam | Users, exams, questions |
| Local → Cloud (Post-exam) | After the exam | Attempts, answers, new users |
Production Appwrite Setup
If you don't have a cloud project yet:
- Create a free account at appwrite.io
- Create a new Project
- Create a Database — note the Database ID
- Run the setup script to auto-create all collections:
powershell
bun run setup:appwrite - Create an API key (Settings → API Keys → all scopes) — copy it
- Fill in your
.env:
envNEXT_PUBLIC_APPWRITE_ENDPOINT=https://cloud.appwrite.io/v1 NEXT_PUBLIC_APPWRITE_PROJECT_ID=your_project_id NEXT_PUBLIC_APPWRITE_DATABASE_ID=your_database_id APPWRITE_API_KEY=your_api_key
Note: Usehttps://cloud.appwrite.io/v1(not a regional endpoint likefra.cloud.appwrite.io).
Local / Offline Exam Setup
Step 1 — Start local Appwrite
powershelldocker compose -f docker/exam-mode/docker-compose.yml up -d
Open http://localhost/console and:
- Create a local Appwrite account (any email + password — this is just for the local console)
- Create a Project → copy the Project ID
- Settings → API Keys → create a key with all scopes → copy it
Tip: Keep the console tab open — you'll need to verify the database got created in Step 3.
Step 2 — Configure .env
Add the following to your.env file:
envEXAM_MODE=true APPWRITE_LOCAL_ENDPOINT=http://localhost/v1 APPWRITE_LOCAL_PROJECT_ID=<from step 1 above> APPWRITE_LOCAL_DATABASE_ID=exam-db APPWRITE_LOCAL_API_KEY=<from step 1 above> EXAM_PASSWORD_PREFIX=exam_2026_
AboutEXAM_PASSWORD_PREFIX: This is the shared password that ALL students use to log in during the exam. Set it to something simple (e.g.109orexam2026) so students can type it easily. Change it each academic year. The value you set here IS the password — nothing is appended to it.
Step 3 — Create local database schema
Run the setup script pointing at your local instance:
This auto-creates thepowershellbun run setup:appwrite:local
exam-db database and all required collections with the correct attributes and indexes.
Verify it worked: Go to http://localhost/console → your project → Databases → you should see exam-db with ~10 collections.
Step 4 — Build the application
powershellbun run build
This compiles the Next.js app with exam mode settings baked in.
Step 5 — Start the dev server (exam day)
Use the single command that starts Docker and the Next.js dev server bound to all interfaces:
powershellbun run exam:start
This is equivalent to:
docker compose -f docker/exam-mode/docker-compose.yml up -d(starts local Appwrite)next dev --hostname 0.0.0.0 --port 3000(students can reach the app over LAN)
Students connect at: http://<your-machine-ip>:3000
Admin dashboard: http://<your-machine-ip>:3000/exam-dashboard
Appwrite console: http://localhost/console
Finding your IP (Windows): Runipconfigin PowerShell and look for theIPv4 Addressunder your active Wi-Fi or Ethernet adapter (e.g.192.168.1.42).
Step 6 — Stop the server
powershellbun run exam:stop
This kills the Next.js process and stops the Docker containers. Student data in Docker volumes is preserved until you explicitly wipe it.
Before the Exam
Pre-exam checklist
- ✅ Connect the exam machine to the same Wi-Fi / LAN students will use
- ✅ Start the server:
bun run exam:start - ✅ Open
/exam-dashboard→ Sync Cloud → Local- First time only: click the 🔑 key icon and enter your
APPWRITE_LOCAL_API_KEYto authenticate - This pulls all registered students, exams, and questions from cloud to local
- First time only: click the 🔑 key icon and enter your
- ✅ Verify exam is published: check the Exams panel on the dashboard
- ✅ Open View Student Credentials → print or distribute the name + password list
- ✅ Test by logging in as one student on another device
Walk-in students (not pre-registered)
Students who didn't register before the exam can be added on the spot using the Register Walk-in Student button on the dashboard, or via the API:
Returns the student'sPOST /api/exam-mode/register-local { "name": "Student Name", "email": "student@example.com", "registration_number": "12345678", "branch": "CSE", "college_name": "Example College" }
userId and examPassword immediately. The student can log in right away.
Walk-in students created locally will be automatically synced to cloud when you run the Post-Exam Sync — no manual steps needed.
During the Exam
Admin dashboard features
| Feature | Description |
|---|---|
| Active Students | Live view of who is currently taking the exam |
| Completed Students | Students who have submitted |
| Force Submit | Instantly submit a student's exam (e.g. if they're cheating) |
| Restart Exam | Clear a student's answers and restart their timer |
| Auto-submit on end time | All in-progress exams auto-submit when the exam's end time passes |
Auto-submit behavior
The system has two layers of auto-submit protection:
- Student-side timer: Each student's timer counts down to zero, then auto-submits. If the exam's end time is sooner than the remaining timer, it uses whichever is shorter.
- Server-side sweep: The admin dashboard fires a background sweep every 15 seconds that force-submits any in-progress attempt whose exam end time has passed — even if the student's browser is closed or frozen.
After the Exam
Post-exam checklist
- ✅ Wait for all students to submit (or force-submit any remaining)
- ✅ Restore internet connectivity on the exam machine
- ✅ Open
/exam-dashboard→ Sync Local → Cloud- This pushes: all exam attempts, student answers, evaluation queue, and any newly registered students
- ✅ Verify the sync report shows the expected numbers
- ✅ Log in to the normal admin panel (cloud) and evaluate/publish results
- ✅ After evaluation is done, wipe local data:
powershell
docker compose -f docker/exam-mode/docker-compose.yml down -v
What gets synced to cloud (Post-Exam Sync)
| Data | Synced? | Notes |
|---|---|---|
| Exam attempts | ✅ Yes | Only submitted / evaluated — not in_progress |
| Student answers | ✅ Yes | All answers for synced attempts |
| Manual evaluation queue | ✅ Yes | Subjective questions needing manual grading |
| New users (walk-ins / local sign-ups) | ✅ Yes | Created in cloud with exam password |
| Existing cloud users | ➡️ Skipped | Already exist — never overwritten or deleted |
Important: Post-Exam Sync never deletes any existing cloud user or data. It only creates missing users and upserts submission records.
Managing the Stack
powershell# ── Start / Stop ───────────────────────────────────────────────────── # Start everything (Docker + dev server) bun run exam:start # Stop everything (Docker containers + kill Next.js process) bun run exam:stop # Start Docker containers only (background, no Next.js) docker compose -f docker/exam-mode/docker-compose.yml up -d # Stop Docker containers (data is preserved in volumes) docker compose -f docker/exam-mode/docker-compose.yml down # ── Data Management ────────────────────────────────────────────────── # Wipe ALL local data — run ONLY after syncing to cloud docker compose -f docker/exam-mode/docker-compose.yml down -v # ── Logs & Status ──────────────────────────────────────────────────── # View Appwrite logs (live) docker logs appwrite -f # Check which containers are running docker ps # Check container health docker compose -f docker/exam-mode/docker-compose.yml ps # ── Database ───────────────────────────────────────────────────────── # Re-create collections after wiping (run after down -v) bun run setup:appwrite:local # ── Sync ───────────────────────────────────────────────────────────── # Pre-exam sync (Cloud → Local) — via dashboard UI or API # POST http://localhost:3000/api/exam-mode/sync-to-local # Post-exam sync (Local → Cloud) — via dashboard UI or API # POST http://localhost:3000/api/exam-mode/sync-to-cloud
Environment Variables
Required — Cloud
| Variable | Description |
|---|---|
NEXT_PUBLIC_APPWRITE_ENDPOINT | Cloud Appwrite endpoint (https://cloud.appwrite.io/v1) |
NEXT_PUBLIC_APPWRITE_PROJECT_ID | Cloud project ID |
NEXT_PUBLIC_APPWRITE_DATABASE_ID | Cloud database ID |
APPWRITE_API_KEY | Cloud admin API key (server-only) |
Required — Exam Mode
| Variable | Description |
|---|---|
EXAM_MODE | true to enable offline exam mode |
APPWRITE_LOCAL_ENDPOINT | Local Appwrite URL (http://localhost/v1) |
APPWRITE_LOCAL_PROJECT_ID | Local project ID |
APPWRITE_LOCAL_DATABASE_ID | Must be exam-db |
APPWRITE_LOCAL_API_KEY | Local admin API key |
EXAM_PASSWORD_PREFIX | The shared exam password all students use to log in |
Optional
| Variable | Description |
|---|---|
EXAM_ALLOWED_IPS | Comma-separated IPs allowed to access exam APIs (blank = allow all) |
Security Notes
- Change
_APP_OPENSSL_KEY_V1and_APP_EXECUTOR_SECRETindocker/exam-mode/appwrite.envto long random strings before any real use - The local Appwrite console (port 80) should only be accessible to the admin machine — block it from student devices using your router/firewall
EXAM_PASSWORD_PREFIXshould be kept secret and changed after each exam season- After syncing to cloud, run
docker compose down -vto wipe all local student data
Troubleshooting
Server won't stop with Ctrl+C
Use the dedicated stop command instead:
powershellbun run exam:stop
If that doesn't work, manually kill port 3000:
powershell# Find the process using port 3000 netstat -ano | findstr :3000 # Kill it (replace <PID> with the number from the above output) taskkill /F /PID <PID>
Students can't connect to the server
- Make sure the exam machine and student devices are on the same network (same Wi-Fi or LAN)
- Find your machine's IP with
ipconfig(look forIPv4 Address) - Check Windows Firewall isn't blocking port 3000:
powershell
# Allow port 3000 through firewall netsh advfirewall firewall add rule name="PlacePrep Exam" dir=in action=allow protocol=TCP localport=3000 - Make sure
bun run exam:startis running (not just Docker)
Pre-exam sync fails with authentication error
The local database is empty so there are no user accounts yet. Use bootstrap mode:
- On the exam dashboard, click the 🔑 key icon
- Enter your
APPWRITE_LOCAL_API_KEYvalue from.env - Click Sync — this bypasses normal auth for the first sync
"exam-db" database not found
The local database schema hasn't been created yet. Run:
Make surepowershellbun run setup:appwrite:local
APPWRITE_LOCAL_PROJECT_ID and APPWRITE_LOCAL_API_KEY are correct in .env first.
Docker containers keep restarting
Check logs for errors:
powershelldocker logs appwrite -f docker logs appwrite-db -f
Common causes:
- Not enough disk space (
docker system dfto check) - Port 80 already in use by another app — stop IIS, Nginx, or XAMPP first
- Corrupted volume data — try
docker compose down -vand restart
Student signed up locally but can't log in after sync to cloud
Students who signed up during exam mode are synced to cloud with the exam password (yourEXAM_PASSWORD_PREFIX value). They should use that to log in to the cloud app. If they want to set a custom password, they can use the "Forgot Password" flow on the cloud app.
Post-sync shows +0 for users but a new student exists locally
Make sure you're running the Post-Exam Sync (Local → Cloud), not the Pre-Exam Sync. The post-sync is the one that pushes new users and attempts to cloud.
Need help? Check the FAQ or Troubleshooting guide.