A forkable, full-stack WebGIS template: a Django/GeoDjango + PostGIS API, a React + MapLibre GL JS (PMTiles) web client, and an Expo React Native mobile client, all in one monorepo and ready to run in containers or deploy to DigitalOcean App Platform.
| Layer | Tech |
|---|---|
| Backend | Django 6.0, Django Ninja, GeoDjango, PostGIS, django-allauth (headless), Celery (optional) |
| Web | React 19, Vite, MapLibre GL JS, PMTiles, Material UI, Zustand, react-router 7 |
| Mobile | Expo 53, React Native, expo-router, react-native-maps, Zustand |
| Infra | Docker Compose (local), DigitalOcean App Platform + Managed Postgres + Spaces (prod) |
WebGIS/
├── backend/ Django + Django Ninja + GeoDjango/PostGIS API
├── frontend/ React + Vite + MapLibre GL JS (PMTiles) web client
├── mobile/ Expo / React Native client (TypeScript)
├── docker-compose.yml Local containerized stack
├── .do/app.yaml DigitalOcean App Platform spec
├── ARCHITECTURE.md System design reference
└── README.md
This is a single git repository. See ARCHITECTURE.md for the full design (components, request/auth flow, deployment topology, and key decisions).
Requires Docker + Docker Compose.
# 1. Configure environment (templates are committed; .env files are gitignored)
cp backend/.env.example backend/.env
cp frontend/.env.example frontend/.env
# 2. Set a real SECRET_KEY in backend/.env, e.g.:
python3 -c "import secrets; print('SECRET_KEY=' + secrets.token_urlsafe(50))"
# 3. Bring up db (PostGIS) + backend + frontend
docker compose up --build- Web client: http://localhost:5173
- API + docs: http://localhost:8000/api/docs
- Admin: http://localhost:8000/admin/ (
docker compose exec backend python manage.py createsuperuser)
The demo geometry models (point/polygon/line) are registered with GeoDjango's
GISModelAdmin, so the admin edit pages render an interactive map widget. To seed a few
sample polygons, run docker compose exec backend python manage.py import_demo_features
(a LayerMapping example that loads backend/api/sample_data/demo_polygons.geojson).
If host port 5432 is in use: POSTGRES_HOST_PORT=5433 docker compose up --build.
For async email via Celery: docker compose --profile celery up.
Backend (needs Python 3.12+, PostgreSQL + PostGIS, and system GDAL/GEOS/PROJ):
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # set SECRET_KEY + POSTGRES_* (or DATABASE_URL)
python manage.py migrate
python manage.py runserver # http://localhost:8000Web client (Node 20+):
cd frontend
cp .env.example .env
npm install
npm run dev # http://localhost:5173Mobile client (Node 20+, Expo):
cd mobile
npm install
npm start # Expo dev server; needs GOOGLE_MAPS_API_KEY in mobile/.envAll configuration is environment-driven. Copy each .env.example to .env and edit:
backend/.env.example—SECRET_KEY,DEBUG,DATABASE_URLorPOSTGRES_*,FRONTEND_URL,ALLOWED_HOSTS,EMAIL_*, optionalREDIS_URL. Email is env-driven and defaults to Django's console backend whenDEBUG=True, so signup/verification emails print to stdout in local dev with no SMTP credentials; setEMAIL_BACKEND+EMAIL_*for real delivery.frontend/.env.example—VITE_API_URL,VITE_MUNI_GEOJSON_URL, and the PMTiles vars (VITE_BASEMAP_PMTILES_URL,VITE_GLYPHS_URL).VITE_*are inlined at build time.
The web map uses a PMTiles vector basemap when VITE_BASEMAP_PMTILES_URL points at a
.pmtiles archive (Protomaps schema), and falls back to raster OpenStreetMap when it is
blank, so the app works out of the box. In production, host the archive on DO Spaces and
configure the bucket CORS to allow Range requests from your origin.
Google login is wired across all three clients via django-allauth's headless token flow; it stays inert until you provide OAuth client IDs. To enable it:
- In the Google Cloud Console, create/select a project and
configure the OAuth consent screen (External; scopes
profile,email; add test users while the app is unverified). - Create OAuth client IDs under APIs & Services → Credentials:
- Web application — Authorized JavaScript origins: your web origins (e.g.
http://localhost:5173and your production domain). - iOS — bundle ID
com.brad.stricherz.WebGISReactNative(change per fork). - Android — package name
com.brad.stricherz.WebGISReactNative+ the signing key SHA-1.
- Web application — Authorized JavaScript origins: your web origins (e.g.
- Set the IDs in each
.env:backend/.env:GOOGLE_OAUTH_CLIENT_ID_WEB(+GOOGLE_OAUTH_SECRET_WEB),GOOGLE_OAUTH_CLIENT_ID_IOS,GOOGLE_OAUTH_CLIENT_ID_ANDROID.frontend/.env:VITE_GOOGLE_CLIENT_ID(the web client ID).mobile/.env:EXPO_PUBLIC_GOOGLE_CLIENT_ID_WEB/_IOS/_ANDROID.
The clients obtain a Google id_token and POST it to /_allauth/app/v1/auth/provider/token;
allauth verifies the JWT signature and audience, then auto-links to an existing account with the
same verified email. (The allauth Google provider requires PyJWT + cryptography, which are in
backend/requirements.txt.) See docs/superpowers/specs/2026-06-04-google-sso-design.md for the
rationale.
.do/app.yaml defines all components against this one repo (a backend service, a frontend
static site, a managed Postgres database, and a pre-deploy migration job).
# set github.repo/branch in .do/app.yaml, then:
doctl apps spec validate .do/app.yaml
doctl apps create --spec .do/app.yamlSet the SECRET-typed env values (SECRET_KEY, EMAIL_*) in the App Platform UI. PostGIS is
enabled automatically on first deploy by the CreateExtension migration. See
ARCHITECTURE.md §7 for details.
The web map composes three kinds of geospatial data; pick whichever fits a given layer:
- PMTiles vector basemap — the background map (Protomaps schema), served from a single
.pmtilesarchive over HTTP range requests. Falls back to raster OSM when unset. - Attribute-rich GeoJSON overlay — small, filterable, attribute-heavy layers (the demo St. Louis municipalities layer) fetched as GeoJSON and filtered client-side.
- This template's own backend API — geospatial data served by the Django API
(
/api/points|polygons|lines) and rendered on the map.
Web layers are config-driven from frontend/src/layers.js: each entry declares its data
source (geojson-url or backend), style, and field metadata. Add or swap a layer by editing
that file — the map, attribute table, sidebar filters, and dashboard all read from it, so no
component changes are needed. The shipped Demo Polygons (backend API) layer is the reference
pattern for consuming the backend.
The backend's /api/gdal/* endpoints parse raster/vector files with GDAL/OGR. The GDAL version
installed by the Docker base distro (3.6.2) has known advisories fixed only in much newer releases.
File paths are confined to GDAL_FILE_ROOT, but treat any file these endpoints touch as trusted
input. To expose them to untrusted users, sandbox GDAL or disable the router (drop the
add_router("/gdal", ...) line in backend/api/api.py). See backend/ReadMe.md.
- The web client is plain JavaScript (no TypeScript); lint with
npm run lint. - Backend tests:
cd backend && pytest(needs a PostGIS database; Django builds an isolated test DB). - Per-component detail lives in
backend/ReadMe.md,frontend/README.md, andmobile/README.md.
MIT © GeoBrad.dev