Connect with fellow learners in the PlacePrep Discord communityㆍ Join now →
R's PlacePrep

Run PlacePrep offline for in-person exams with Docker and a local Appwrite: LAN setup, pre-exam sync, auto-submit and post-exam sync.

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
  • Ports 80 and 3000 free on the exam machine
  • 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=exam2026
About EXAM_PASSWORD_PREFIX: Despite the name, this is not a prefix — the value you set here is the password, and nothing is appended to it. All students share it during the exam, so pick something short and easy to type (e.g. exam2026). Change it each academic year. If the variable is unset, it defaults to exam_2026_.

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, all 16 collections with their attributes and indexes, and the tnp_documents storage bucket. It is safe to re-run — anything that already exists is skipped. Verify it worked: Go to http://localhost/console → your project → Databases → you should see exam-db with 16 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-dashboard → Pre-Exam Sync (Cloud → Local)
    • First time only: the Bootstrap Sync dialog asks for your APPWRITE_LOCAL_API_KEY, since the local database has no accounts yet
    • This pulls all registered students, exams, and questions from cloud to local
  4. ✅ Verify the exam appears in the dashboard's exam filter, and that it is published
  5. ✅ Note the shared Student Password shown in the Exam Mode Status card and tell students what it is
  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 simply sign up on the local app — their account is created in the local database and pushed to cloud during the post-exam sync.

You can also create the account directly:

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.

There is no button for this on the dashboard yet — it is an API-only endpoint.

Walk-in students created locally are automatically synced to cloud when you run the Post-Exam Sync — no manual steps needed.


During the Exam

Admin dashboard features

FeatureDescription
Exam Mode StatusShows whether exam mode is on, the local endpoint, project and database IDs, the shared student password, and allowed IPs
Data SyncPre-Exam Sync (Cloud → Local) and Post-Exam Sync (Local → Cloud), with a per-collection report
Exam FilterChoose which live exam to monitor
Active StudentsLive view of who is currently taking the exam
Student ProgressPer-student progress within the exam
Force SubmitInstantly submit a student's exam
Restart ExamClear a student's answers and restart their timer
Completed StudentsStudents who have submitted, with the option to delete an attempt
Auto-refresh (15s)Refreshes the lists and runs the auto-submit sweep

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. The effective time is whichever is shorter: their remaining duration, or the time left until the exam's end time.
  2. Dashboard sweep: while the exam dashboard is open with Auto-refresh (15s) enabled, it fires a 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.

The sweep is driven by the open dashboard tab, not by a cron job. Keep the dashboard open on the exam machine for the duration of the exam.


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-dashboard → Sync 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: the dashboard shows a Bootstrap Sync dialog asking for a local API key. Enter your APPWRITE_LOCAL_API_KEY value from .env and run the 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-mariadb -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.

Found a problem with this page? Help us fix it.

Still stuck? Read the FAQ or the troubleshooting guide.