Running ConnectedCampus on Windows without Docker
For a Windows machine where Docker Desktop itself is the problem — commonly an 8GB laptop, where Docker Desktop's WSL2 VM reservation defaults to up to 50% of host RAM regardless of what's actually running inside it. This walks through running the full dev stack with zero Docker containers, step by step, either against Postgres installed directly on this machine or — usually less setup — against a team's already-deployed shared database over WireGuard (step 5 covers both).
Why this goes through WSL2, not "native Windows"
This repo's Postgres needs two extensions compiled against it from source: pgvector and
Apache AGE (shared_preload_libraries = 'age') — no prebuilt binary ships either for Windows,
and Apache AGE in particular has no realistic native-Windows build path (its build system expects a
Unix toolchain). WSL2 is a real Linux kernel, not emulation, so it's the one place on a Windows
machine this can be built and run exactly the way infra/postgres/Dockerfile already does it for
Docker — same apt packages, same make && make install steps, just on the host filesystem instead
of inside a container. There is no Docker anywhere in this path: not for Postgres, not for the Bun
apps. WSL2 is the OS underneath, the same way it always is for a Docker Desktop setup — the
difference here is nothing runs containerized on top of it.
If you've never used WSL2 before: it's a lightweight Linux environment that runs alongside Windows (not a VM you manage yourself, not dual-boot) — you keep using Windows normally, and open a Linux terminal when you need one. Editing files from VS Code's Remote - WSL extension works exactly like editing local files.
1. Install WSL2 + Ubuntu
From PowerShell as Administrator (Windows side):
wsl --install
This installs WSL2 and Ubuntu (the default distro) in one step on a current Windows 10/11 build. Restart when prompted. On first launch of the "Ubuntu" app from the Start menu, it finishes setup and asks you to create a Linux username/password — this is separate from your Windows login, and only exists inside WSL2.
If wsl --install reports WSL is already installed but no distro is present, run
wsl --install -d Ubuntu instead.
Everything from here on runs inside the Ubuntu (WSL2) terminal, not PowerShell/cmd.
2. Work inside the Linux filesystem, not /mnt/c
Clone the repo under your WSL2 home directory (e.g. ~/ConnectedCampus), not under
/mnt/c/Users/.... Files under /mnt/c are on the Windows filesystem accessed through a
translation layer — bun install and Postgres both do a lot of small file I/O, and that layer
makes it dramatically slower. Working entirely inside ~ avoids this; VS Code's Remote-WSL
extension opens ~/ConnectedCampus directly with no translation either.
cd ~
3. Install Bun and git
Ubuntu ships git already; if not: sudo apt-get update && sudo apt-get install -y git.
curl -fsSL https://bun.sh/install | bash
exec $SHELL # reload so `bun` is on PATH
bun --version # should be 1.4+ — the repo pins bun@1.4.2 in package.json
4. Clone and install
git clone <repo-url> ConnectedCampus
cd ConnectedCampus
bun install
cp .env.example .env
Set BETTER_AUTH_SECRET in .env to any random string (openssl rand -hex 32) — auth will not
work without it. Also set INTERNAL_SERVICE_TOKEN (same command) — apps/agents and
apps/credentials fail closed (503 on everything but /health) without it. See
Getting Started for what every other variable does; nothing about them changes
on this path.
5. Set up Postgres — two options
If your team already has ConnectedCampus deployed on a shared VPS, use Option A — it's less work (no compiling extensions from source) and puts you on the same real data as everyone else. Use Option B only if you're working offline, or there's no shared instance yet.
Option A — connect to the team's shared cloud database (recommended)
Postgres already exists on the VPS, reachable only through a WireGuard tunnel — never on the public internet (see Remote Postgres over WireGuard for the full reasoning). Nothing to compile or install except the WireGuard client itself.
Ask whoever administers the VPS for two things, sent through a secure channel (not chat/email in plaintext — both are credentials):
- A WireGuard peer config — they run
./infra/wireguard/add-peer.sh <your-name>on the VPS and send you the resulting.conffile. - The real
DATABASE_URLandRUNTIME_DATABASE_URLvalues for the shared database.
Install and bring the tunnel up, inside WSL2 — not the Windows WireGuard app. The Bun processes you're about to run live inside WSL2's own network namespace, which is separate from Windows': a tunnel brought up only through the Windows-side app would not be reachable from them.
sudo apt-get install -y wireguard-tools
mkdir -p ~/.wireguard
# save the .conf file you were given as ~/.wireguard/<your-name>.conf, then:
chmod 600 ~/.wireguard/<your-name>.conf
sudo wg-quick up ~/.wireguard/<your-name>.conf
Verify it's actually connected, not just configured:
sudo wg show # a recent "latest handshake" line confirms it's live
ping -c 2 10.8.0.1 # the VPS's tunnel address
pg_isready -h 10.8.0.1 -p 55432 # the shared Postgres, reachable only through this tunnel
Then in .env (after cp .env.example .env), replace DATABASE_URL and RUNTIME_DATABASE_URL
with the real values you were given — same shape as below, with a real password instead of
<password>:
DATABASE_URL=postgres://campus:<password>@10.8.0.1:55432/campus_os
RUNTIME_DATABASE_URL=postgres://campus_app:<password>@10.8.0.1:55432/campus_os
Skip step 6 below (Migrate and seed) — the schema already exists on the shared database, and running migrations isn't something every developer needs to do day-to-day. Go straight to step 7.
This is the same live data every other developer and the deployed app itself use — not a personal
sandbox. Anything your local API writes while testing (creating a student, editing attendance,
etc.) is a real write everyone else will see. Bring the tunnel down when you're done for the
session: sudo wg-quick down ~/.wireguard/<your-name>.conf.
Option B — install Postgres natively on this machine
./infra/postgres-native/setup.sh
This is the script referenced in Getting Started — it adds the PGDG apt
repo, installs Postgres 18, builds pgvector and apache/age from source, configures
shared_preload_libraries = 'age', and creates the campus role + campus_os database. It
listens on port 55432 (matching docker-compose.yml's mapping), so .env.example's
DATABASE_URL / RUNTIME_DATABASE_URL need zero changes. Takes a few minutes the first time
(compiling two Postgres extensions from source); safe to re-run if it fails partway through.
It ends by verifying both extensions are actually loaded:
select extname from pg_extension where extname in ('vector', 'age') order by extname;
Confirm you see both age and vector in the output before continuing.
6. Migrate and seed
Option A only skips this step — the shared database is already migrated and seeded. If you set up Postgres natively (Option B), run this once:
bun run db:generate # only if you changed the Drizzle schema — skip on a fresh clone
bun run db:migrate # applies migrations, RLS policies, then creates campus_app
bun run db:seed # seeds the 20 KIOT module_definitions rows
7. Run it
bun run dev
Turborepo runs every app in parallel — apps/web at
http://localhost:3000. WSL2 forwards localhost automatically on current
Windows builds, so this works the same from a Windows browser as it would from inside WSL2 itself —
no port-forwarding setup needed.
If you only need one service (lightest possible setup — Postgres + one Bun process):
bun --filter '@campus/api' dev
curl http://localhost:4000/health
There is still no tenant/admin user at this point — either run the import below, or create one by hand; see Getting Started for both options.
8. Cap WSL2's memory (recommended either way)
Even without Docker Desktop's reservation, WSL2 itself can grow to use most of the host's RAM under
load. Cap it explicitly — create/edit %UserProfile%\.wslconfig on the Windows side (a
Windows-side file, not something you create from inside WSL2):
[wsl2]
memory=6GB
processors=4
Then from PowerShell: wsl --shutdown, and reopen your Ubuntu terminal for it to take effect.
Optional: importing the real legacy institution data
Skip this if you're on Option A — the shared database already has the real data imported; importing again would be redundant (and the import scripts assume they're the only writer).
For Option B (a fresh, empty local Postgres): Getting Started's
import pipeline loads a legacy MySQL backup — and that step still uses a throwaway Docker MySQL
container (infra/import/docker-compose.mysql.yml), just to load mysqldump output reliably. If
you're committed to zero Docker on this machine, skip the import entirely and start with an empty
institution instead — everything in steps 1–7 above works fully without it. If you do want the real
data, that one step needs Docker installed for its duration; nothing else in this guide does.
Troubleshooting
bun installor Postgres build fails with a permissions error inside/mnt/c— you're working outside the Linux filesystem; re-clone under~(step 2).- Git shows every file as modified with no real changes — usually CRLF line endings from a
Windows-side clone leaking in. Run
git config --global core.autocrlf inputinside WSL2 before cloning. localhost:3000doesn't load from a Windows browser — confirm you're on WSL2 (wsl -l -vfrom PowerShell should showVERSION 2, not1); WSL1 doesn't forward localhost the same way.- Port
55432(or5432) already in use (Option B only) — something else is already listening; check withsudo ss -tlnp | grep 5432and stop or reconfigure it before re-runninginfra/postgres-native/setup.sh. .wslconfigchanges don't seem to apply — they only take effect afterwsl --shutdownfully stops the WSL2 VM (not just closing the terminal window) and it's restarted.- (Option A)
pg_isready -h 10.8.0.1 -p 55432times out — the tunnel isn't actually up. Checksudo wg showfor a recent handshake; if there's none, re-runsudo wg-quick up ~/.wireguard/<your-name>.confand check its output for errors (a common one: the.conffile's permissions aren't600, or it was saved with Windows CRLF line endings — re-save it with Unix line endings ifwg-quickcomplains about parsing it). - (Option A) Connected, but the app can't reach the database from inside WSL2 — confirm the
tunnel was brought up with
wg-quickinside the WSL2 terminal, not the Windows WireGuard app; see the note in step 5's Option A about the two having separate network namespaces.
Verifying everything works
bun run typecheck # across every package
bun run test # needs Postgres up — rls.test.ts hits the real database
bun run test will fail if Postgres isn't reachable or the extensions aren't loaded — same check as
step 5's verification (either option), exercised for real. If you're on Option A, this runs
against the real shared database — the RLS test itself only reads/asserts, but be aware any other
test that writes data would be a real write there too.