A public gallery of downloadable GPX cycling routes with an admin panel for uploading and managing routes.
git clone https://github.com/gverbist/GPX_Routes_Web.git
cd GPX_Routes_Web
cp .env.example .env
Edit .env and set the required values:
# REQUIRED: Generate a secret for session signing
# Run: openssl rand -base64 32
BETTER_AUTH_SECRET="your-generated-secret-here"
# REQUIRED: The public URL where the app will be accessible
BETTER_AUTH_URL="https://routes.example.com"
# REQUIRED: Cloudflare Tunnel token (see "Cloudflare Tunnel setup" below)
TUNNEL_TOKEN="your-tunnel-token-here"
# Admin credentials (used to seed the first admin user)
ADMIN_EMAIL="[email protected]"
ADMIN_PASSWORD="changeme123"
ADMIN_NAME="Admin"
# Optional: change the MariaDB root password (only used internally)
MARIADB_ROOT_PASSWORD="root_password"
docker compose up -d
This will:
The app will be available at the domain you configured in the Cloudflare Tunnel.
docker compose ps
docker compose logs app
Pull the latest changes and rebuild:
git pull
docker compose up -d --build
Migrations run automatically on container startup, so schema changes are applied on deploy.
Two Docker volumes are used:
| Volume | Contents |
|---|---|
mariadb_data |
Database files |
gpx_storage |
Uploaded GPX files (/app/storage/gpx/) |
These persist across container restarts and rebuilds.
The app uses a Cloudflare Tunnel to securely expose the app to the internet without opening ports or configuring a reverse proxy. Cloudflare handles TLS automatically.
To get your tunnel token:
gpx-routes).env file as TUNNEL_TOKENroutes.example.com)app:3000 (the Docker service name and port)Make sure BETTER_AUTH_URL in .env matches the public URL (e.g. https://routes.example.com).
The cloudflared container connects outbound to Cloudflare's network — no inbound ports need to be open on your server.
docker compose down # stop containers, data preserved in volumes
docker compose down -v # stop and DELETE all data (removes volumes)
This means the BETTER_AUTH_URL in your .env file doesn't match the public URL where the app is accessed. SvelteKit's CSRF protection compares the request Origin header against the configured origin.
Fix: Double-check your .env file and ensure BETTER_AUTH_URL matches your public domain exactly:
# Must match the URL users visit in their browser
BETTER_AUTH_URL="https://routes.example.com"
Then restart the app container:
docker compose up -d app
If you see a 401 error referencing docs.stadiamaps.com/authentication, the app may still be configured to use Stadia Maps tiles which require domain authentication. The current setup uses OpenStreetMap tiles which work without auth. If you've customised the tile provider, ensure your domain is registered with the tile service.
You may see: failed to sufficiently increase receive buffer size. This is a non-fatal warning from the QUIC library — the tunnel still works. To silence it, increase the host's UDP buffer size:
sudo sysctl -w net.core.rmem_max=7500000
sudo sysctl -w net.core.wmem_max=7500000
To persist across reboots, add to /etc/sysctl.conf:
net.core.rmem_max=7500000
net.core.wmem_max=7500000
# Install dependencies
npm install
# Start MariaDB
docker compose up -d mariadb
# Configure environment
cp .env.example .env
# Edit .env: set BETTER_AUTH_SECRET (openssl rand -base64 32)
# Run database migrations
npx prisma migrate dev
# Seed admin user
npx prisma db seed
# Start dev server
npm run dev
The dev server runs at http://localhost:5173.
npm run build # Production build
npm run preview # Preview production build locally
npx prisma generate # Regenerate Prisma client after schema changes
npx prisma studio # Visual database explorer
src/
routes/
+page.svelte # Public gallery
routes/[id]/
+page.svelte # Route detail (map + stats + download)
download/+server.ts # GPX file download endpoint
admin/
login/ # Admin login
(app)/
routes/ # Admin route list
routes/new/ # Upload GPX + metadata
routes/[id]/edit/ # Edit route
lib/
components/
RouteMap.svelte # Leaflet map (SSR-safe)
ui/ # shadcn-svelte components
server/
auth.ts # Better Auth config
db.ts # Prisma client
gpx-parser.ts # GPX parsing + metadata extraction
schemas/
route.ts # Zod validation + ride type labels
prisma/
schema.prisma # Database schema
seed.ts # Admin user seeder
migrations/ # Database migrations
storage/gpx/ # Uploaded GPX files (gitignored)