Connect with fellow learners in the PlacePrep Discord community Join now →

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:

EnvironmentUsed forDatabase
Cloud (Appwrite Cloud)Normal usage, production dataCloud Appwrite
Local (Docker on your machine)Offline exams on LANLocal Appwrite

The two environments use the same schema and same user IDs, so data syncs cleanly between them.

Sync directions:

DirectionWhenWhat moves
Cloud → Local (Pre-exam)Before the examUsers, exams, questions
Local → Cloud (Post-exam)After the examAttempts, answers, new users

Production Appwrite Setup

If you don't have a cloud project yet:

  1. Create a free account at appwrite.io
  2. Create a new Project
  3. Create a Database — note the Database ID
  4. Run the setup script to auto-create all collections:
    powershell
    bun run setup:appwrite
  5. Create an API key (Settings → API Keys → all scopes) — copy it
  6. Fill in your .env:
env
NEXT_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: Use https://cloud.appwrite.io/v1 (not a regional endpoint like fra.cloud.appwrite.io).

Local / Offline Exam Setup

Step 1 — Start local Appwrite

powershell
docker compose -f docker/exam-mode/docker-compose.yml up -d

Open http://localhost/console and:

  1. Create a local Appwrite account (any email + password — this is just for the local console)
  2. Create a Project → copy the Project ID
  3. 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:
env
EXAM_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_
About EXAM_PASSWORD_PREFIX: This is the shared password that ALL students use to log in during the exam. Set it to something simple (e.g. 109 or exam2026) 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:

powershell
bun run setup:appwrite:local
This auto-creates the 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

powershell
bun 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:

powershell
bun 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): Run ipconfig in PowerShell and look for the IPv4 Address under your active Wi-Fi or Ethernet adapter (e.g. 192.168.1.42).

Step 6 — Stop the server

powershell
bun 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

  1. ✅ Connect the exam machine to the same Wi-Fi / LAN students will use
  2. ✅ Start the server: bun run exam:start
  3. ✅ Open /exam-dashboardSync Cloud → Local
    • First time only: click the 🔑 key icon and enter your APPWRITE_LOCAL_API_KEY to authenticate
    • This pulls all registered students, exams, and questions from cloud to local
  4. ✅ Verify exam is published: check the Exams panel on the dashboard
  5. ✅ Open View Student Credentials → print or distribute the name + password list
  6. ✅ 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:

POST /api/exam-mode/register-local
{
  "name": "Student Name",
  "email": "student@example.com",
  "registration_number": "12345678",
  "branch": "CSE",
  "college_name": "Example College"
}
Returns the student's 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

FeatureDescription
Active StudentsLive view of who is currently taking the exam
Completed StudentsStudents who have submitted
Force SubmitInstantly submit a student's exam (e.g. if they're cheating)
Restart ExamClear a student's answers and restart their timer
Auto-submit on end timeAll in-progress exams auto-submit when the exam's end time passes

Auto-submit behavior

The system has two layers of auto-submit protection:

  1. 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.
  2. 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

  1. ✅ Wait for all students to submit (or force-submit any remaining)
  2. ✅ Restore internet connectivity on the exam machine
  3. ✅ Open /exam-dashboardSync Local → Cloud
    • This pushes: all exam attempts, student answers, evaluation queue, and any newly registered students
  4. ✅ Verify the sync report shows the expected numbers
  5. ✅ Log in to the normal admin panel (cloud) and evaluate/publish results
  6. ✅ 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)

DataSynced?Notes
Exam attempts✅ YesOnly submitted / evaluated — not in_progress
Student answers✅ YesAll answers for synced attempts
Manual evaluation queue✅ YesSubjective questions needing manual grading
New users (walk-ins / local sign-ups)✅ YesCreated in cloud with exam password
Existing cloud users➡️ SkippedAlready 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

VariableDescription
NEXT_PUBLIC_APPWRITE_ENDPOINTCloud Appwrite endpoint (https://cloud.appwrite.io/v1)
NEXT_PUBLIC_APPWRITE_PROJECT_IDCloud project ID
NEXT_PUBLIC_APPWRITE_DATABASE_IDCloud database ID
APPWRITE_API_KEYCloud admin API key (server-only)

Required — Exam Mode

VariableDescription
EXAM_MODEtrue to enable offline exam mode
APPWRITE_LOCAL_ENDPOINTLocal Appwrite URL (http://localhost/v1)
APPWRITE_LOCAL_PROJECT_IDLocal project ID
APPWRITE_LOCAL_DATABASE_IDMust be exam-db
APPWRITE_LOCAL_API_KEYLocal admin API key
EXAM_PASSWORD_PREFIXThe shared exam password all students use to log in

Optional

VariableDescription
EXAM_ALLOWED_IPSComma-separated IPs allowed to access exam APIs (blank = allow all)

Security Notes

  • Change _APP_OPENSSL_KEY_V1 and _APP_EXECUTOR_SECRET in docker/exam-mode/appwrite.env to 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_PREFIX should be kept secret and changed after each exam season
  • After syncing to cloud, run docker compose down -v to wipe all local student data

Troubleshooting

Server won't stop with Ctrl+C

Use the dedicated stop command instead:

powershell
bun 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

  1. Make sure the exam machine and student devices are on the same network (same Wi-Fi or LAN)
  2. Find your machine's IP with ipconfig (look for IPv4 Address)
  3. 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
  4. Make sure bun run exam:start is 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:

  1. On the exam dashboard, click the 🔑 key icon
  2. Enter your APPWRITE_LOCAL_API_KEY value from .env
  3. 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:

powershell
bun run setup:appwrite:local
Make sure APPWRITE_LOCAL_PROJECT_ID and APPWRITE_LOCAL_API_KEY are correct in .env first.

Docker containers keep restarting

Check logs for errors:

powershell
docker logs appwrite -f
docker logs appwrite-db -f

Common causes:

  • Not enough disk space (docker system df to check)
  • Port 80 already in use by another app — stop IIS, Nginx, or XAMPP first
  • Corrupted volume data — try docker compose down -v and 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 (your EXAM_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.