Lokasi Tepat, Produktivitas Hebat!
About Us
·
Vote Project Now
·
Report Bug
PlaceRadar is a hyper-specific workspace discovery platform for students and remote workers in Sleman, Yogyakarta. It highlights practical amenities such as Wi-Fi stability, outlet availability, and ambient noise so users can quickly choose the best "third place" for focus work.
- Architecture Overview
- Tech Stack
- Project Structure
- Prerequisites
- Getting Started
- Accessing the Application
- Environment Configuration
- Authentication
- Reviews
- Development Workflow
- Seeding Sample Data
- Key Application Modules
- Infrastructure Services
- TLS Certificate Setup
- License
The application runs as six Docker services defined in docker-compose.yml:
| Service | Container | Description |
|---|---|---|
| Frontend | ReactJS | React 19 + Vite SPA |
| Backend | ExpressJS | Node.js REST API |
| Database | PostgreSQL | Relational database |
| Reverse Proxy | nginx | TLS termination & routing |
| Monitoring | NagiOS | Service health checks |
| DNS | CoreDNS | Local domain resolution |
- Frontend: React 19 + Vite 7 + @react-oauth/google (
frontend/package.json) - Backend: Node.js + Express 5 + Sequelize + bcryptjs + google-auth-library + @aws-sdk/client-s3 (
backend/package.json) - Database: PostgreSQL (latest)
- Storage: Cloudflare R2 (S3-compatible Object Storage)
- Reverse Proxy: Nginx with TLS (
infrastructure/nginx/nginx.conf) - DNS Server: CoreDNS (
infrastructure/coredns/Corefile) - Monitoring: Nagios (
infrastructure/nagios/placeradar.cfg) - Tunnel (optional): Ngrok (
infrastructure/ngrok/ngrok.yml)
PlaceRadar/
├── backend/ # Express.js REST API
│ ├── src/
│ │ ├── controllers/ # HTTP request handlers
│ │ ├── models/ # Sequelize models
│ │ ├── routes/ # API route definitions
│ │ ├── services/ # Business logic
│ │ ├── config/ # Database & environment config
│ │ └── middleware/ # Error handling
│ └── Dockerfile
├── frontend/ # React SPA
│ ├── src/
│ │ ├── components/ # Reusable UI components
│ │ ├── context/ # React Context providers
│ │ ├── hooks/ # Custom hooks
│ │ ├── pages/ # Route pages
│ │ └── styles/ # Global CSS
│ └── Dockerfile
├── infrastructure/
│ ├── nginx/ # Reverse proxy config + TLS certs
│ ├── coredns/ # DNS server config
│ ├── nagios/ # Monitoring config
│ └── ngrok/ # Tunnel config (optional)
├── scripts/seed/ # Sample data
├── docker-compose.yml
└── .env.example
- Docker Desktop ≥ 24
- Docker Compose v2
- Node.js 18+ (only for local development without Docker)
-
Clone the repository
git clone https://github.com/ZanDev32/PlaceRadar.git cd PlaceRadar -
Copy environment template
cp .env.example .env
-
Launch the full stack
docker compose up -d --build
-
Verify all containers are running
docker ps
The main address for the app is placeradar.lvh.me (recommended). This is a "magic" domain that automatically resolves to 127.0.0.1 on any OS without configuration.
| Service | URL | Notes |
|---|---|---|
| Frontend (HTTPS) | https://placeradar.lvh.me | Accept self-signed cert warning |
| Frontend (HTTP) | http://placeradar.lvh.me | Served over HTTP (no redirect) |
| API | https://placeradar.lvh.me/api/place | REST endpoints |
| Nagios | https://placeradar.lvh.me/nagios | Monitoring dashboard |
| pgAdmin | https://placeradar.lvh.me/pgadmin | Database UI |
Edit .env to configure the application. Key variables:
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
postgres://postgres:postgres@postgres:5432/placeradar |
PostgreSQL connection string |
POSTGRES_USER |
postgres |
PostgreSQL user created on first init |
POSTGRES_PASSWORD |
postgres |
PostgreSQL password created on first init |
POSTGRES_DB |
placeradar |
PostgreSQL database created on first init |
PGADMIN_DEFAULT_EMAIL |
[email protected] |
Initial pgAdmin login (first init only) |
PGADMIN_DEFAULT_PASSWORD |
admin123 |
Initial pgAdmin password (first init only) |
NAGIOS_USERNAME |
nagiosadmin |
Nagios web UI username |
NAGIOS_PASSWORD |
admin123 |
Nagios web UI password |
NGROK_AUTH_TOKEN |
- | Ngrok authentication (optional) |
JWT_SECRET |
your_jwt_secret |
Secret key for signing JWT tokens |
JWT_EXPIRES_IN |
1h |
JWT expiration, e.g., 15m, 1h, 7d |
ADMIN_USERNAME |
admin |
Admin login for JWT issuance |
ADMIN_PASSWORD |
admin123 |
Admin password for JWT issuance |
VITE_MAPBOX_TOKEN |
- | Mapbox access token (required for frontend maps) |
GOOGLE_CLIENT_ID |
- | Google OAuth Client ID (Backend verification) |
VITE_GOOGLE_CLIENT_ID |
- | Google OAuth Client ID (Frontend widget) |
OPENAI_API_KEY |
- | Backend-only key for AI review summaries (server-side, do not expose to frontend) |
OPENAI_MODEL |
gpt-4o-mini |
OpenAI model used for summaries |
OPENAI_BASE_URL |
https://api.openai.com/v1 |
Override when using a compatible gateway |
R2_ACCOUNT_ID |
- | Cloudflare Account ID |
R2_ACCESS_KEY_ID |
- | Cloudflare R2 Access Key ID |
R2_SECRET_ACCESS_KEY |
- | Cloudflare R2 Secret Access Key |
R2_BUCKET_NAME |
placeradar-images |
Name of the R2 bucket |
R2_PUBLIC_URL |
- | Public URL for accessing images (Backend) |
VITE_R2_PUBLIC_URL |
- | Public URL for accessing images (Frontend) |
The backend reads configuration from backend/src/config/env.js. Review summaries call OpenAI from the backend only; keep OPENAI_API_KEY out of frontend env files.
Images are stored in Cloudflare R2 (S3-compatible storage) and accessed via a public URL.
- Uploads: Handled via
multer(memory storage) and processed withsharp(converted to WebP). - Storage Service:
backend/src/services/storage.service.jsmanages S3 interactions. - Database: Stores the relative path (key) of the image, e.g.,
Place_image/Cafe Name/1.webp.
To display images in the frontend, you must construct the full URL using the VITE_R2_PUBLIC_URL environment variable.
Helper Function Example:
const buildImageUrl = (imagePath) => {
if (!imagePath) return null;
// If it's already a full URL, return it
if (/^https?:\/\//i.test(imagePath)) return imagePath;
// Get base URL from env, remove trailing slash
const base = import.meta.env.VITE_R2_PUBLIC_URL?.replace(/\/$/, '') ?? '';
// Encode path segments to handle spaces and special characters
const encodedPath = imagePath
.split('/')
.map((segment) => encodeURIComponent(segment))
.join('/');
return base ? `${base}/${encodedPath}` : encodedPath;
};Usage in Component:
const imageUrl = buildImageUrl(location.images[0]);
return (
<img src={imageUrl} alt={location.name} />
);Important: Ensure VITE_R2_PUBLIC_URL is set in your .env file and passed to the frontend container (via docker-compose.yml and Dockerfile).
The application supports both traditional username/password authentication and Google OAuth.
- User: Can register, login, and view their profile.
- Admin: Has additional privileges (e.g., managing locations).
- Register:
POST /api/auth/register- Body:
{ "username": "...", "email": "...", "password": "..." }
- Body:
- Login:
POST /api/auth/login- Body:
{ "username": "...", "password": "..." }
- Body:
- Google Login:
POST /api/auth/google- Body:
{ "token": "<google_id_token>" }
- Body:
- Get Profile:
GET /api/auth/me- Header:
Authorization: Bearer <jwt>
- Header:
- Update Profile:
PATCH /api/auth/me- Header:
Authorization: Bearer <jwt> - Body:
{ "firstName": "...", "lastName": "...", "bio": "...", "preferences": [...] }
- Header:
- Upload Avatar:
POST /api/auth/me/avatar- Header:
Authorization: Bearer <jwt> - Body:
FormDatawithimagefile
- Header:
The system supports a bootstrapped admin user via environment variables (ADMIN_USERNAME, ADMIN_PASSWORD). This user has the admin role and can be used for initial setup.
PostgreSQL and pgAdmin both store credentials in Docker volumes on first initialization.
Changing .env values later will update container environment variables, but it will not automatically rewrite existing users/passwords.
- pgAdmin:
PGADMIN_DEFAULT_EMAIL/PGADMIN_DEFAULT_PASSWORDonly apply on first init.- To change the credential without deleting data:
docker compose stop pgadmin docker compose rm -f pgadmin docker volume rm placeradar_pgadmin-data docker compose up -d pgadmin
- To change the credential without deleting data:
- PostgreSQL:
POSTGRES_USER/POSTGRES_PASSWORDonly applies on first init.- To change the credential without deleting data:
docker compose down postgres docker volume rm placeradar_postgres-data docker compose up -d postgres
- To change the credential without deleting data:
User-submitted comments for locations (no rating system). Reviews cannot be edited; authors can delete their own review.
POST /api/reviews/:placeId(auth) — body:{ "comment": "...", "images": ["..."] }GET /api/reviews/:placeId— list all reviews for a location, newest firstDELETE /api/reviews/:reviewId(auth, owner) — remove a review and decrement locationreview_count
POST /api/place/:placeId/review-summary/generate— start summary generation (returns queued/running immediately)GET /api/place/:placeId/review-summary— poll status/results; returns summary text, status, and errors if any- Regenerates automatically when the location
review_countchanges; shows loading/error states in the UI.
cd frontend
npm install
npm run dev # Development server with HMR
npm run build # Production build
npm run serve # Preview production buildcd backend
npm install
npm run dev # Development with nodemon
npm start # Production mode
npm test # Run Jest testsdocker compose up -d --build # Build and start all services
docker compose logs -f # Follow logs
docker compose down # Stop all services
docker compose down -v # Stop and remove volumesQuick start (recommended):
./scripts/seed/run_all_seeds.shStep-by-step seeding order:
- Build/rebuild the backend container (if not done yet):
docker compose build backend- Run SQL seeds first (creates PostGIS extensions/schema and helper data):
./scripts/seed/run_sql_seeds.sh- Seed app locations from JSON (after SQL seeds):
docker exec -it ExpressJS npm run seed:locations- Seed sample users (optional):
docker exec -it ExpressJS npm run seed:users- Seed sample reviews (optional, requires locations + users):
docker exec -it ExpressJS npm run seed:reviewsAlternative: run all backend JSON seeders in one command (after SQL seeds):
docker exec -it ExpressJS npm run seed:allNotes and troubleshooting:
- SQL seeds assume the
postgrescontainer name; overridePOSTGRES_USER/POSTGRES_DBenv vars if your setup differs. - The location seeder is fail-fast: it validates the JSON and stops on the first error.
- It runs in sync mode: upserts rows from
scripts/seed/locations.jsonand deletes anyLocationrows not in the file. - The review seeder prefers
scripts/seed/reviews.json(recommended). Format per entry:locationId(UUID fromscripts/seed/locations.json)userEmail(must exist inscripts/seed/users.json)comment(string)images(array, typically[])- If
scripts/seed/reviews.jsonis missing, it falls back to generating one default review per location.
- DB connection:
- In Docker, ensure the postgres container is running (e.g.,
docker compose up -d postgresor the full stack). The hostnamepostgresmust resolve inside the network. - Outside Docker/local DB, set
DATABASE_URLinbackend/.envto your local instance, e.g.postgres://user:pass@localhost:5432/placeradar, then rerunnpm run seed:locations.
- In Docker, ensure the postgres container is running (e.g.,
Promoted ads (dummy data):
- The promoted cards endpoint reads from
scripts/seed/ads_dummy.json. - The backend serves ad image files from
/api/static/ads/<filename>.- In Docker, this is wired by volume-mounting
frontend/src/assets/adsinto the backend container.
- In Docker, this is wired by volume-mounting
| Module | Path | Description |
|---|---|---|
| Location Model | backend/src/models/Location.js | Sequelize model for workspace locations |
| User Model | backend/src/models/User.js | Sequelize model for users |
| Review Model | backend/src/models/Review.js | Sequelize model for location reviews (comments + images, no ratings) |
| Location Service | backend/src/services/locations.service.js | CRUD business logic |
| Location Controller | backend/src/controllers/locations.controller.js | HTTP request handlers for locations |
| Reviews Controller | backend/src/controllers/reviews.controller.js | Create, list, delete reviews |
| Auth Controller | backend/src/controllers/auth.controller.js | Authentication logic (Login, Register, Google) |
| Location Routes | backend/src/routes/locations.routes.js | Location endpoint definitions |
| Reviews Routes | backend/src/routes/reviews.routes.js | Review endpoint definitions |
| Module | Path | Description |
|---|---|---|
| Location Context | frontend/src/context/LocationContext.jsx |
Global state management |
| useApi Hook | frontend/src/hooks/useApi.js |
API data fetching |
| Home Page | frontend/src/pages/Home.jsx |
Location listing |
| Login Page | frontend/src/pages/Login.jsx |
User authentication page |
- Terminates TLS on port 443
- Serves React static files from
/ - Proxies
/apirequests to the Express backend - Redirects HTTP → HTTPS
Resolves local development domains. Configuration: infrastructure/coredns/Corefile
placeradar.lvh.me → 127.0.0.1
api.placeradar.lvh.me → 127.0.0.1
To use CoreDNS, set your OS DNS server to 127.0.0.1.
Monitors service health. Access at https://placeradar.lvh.me/nagios
Expose local development externally. Set NGROK_AUTH_TOKEN in .env.
The project includes a self-signed certificate for HTTPS development. Browsers will show a warning which you can bypass.
To regenerate the certificate:
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout infrastructure/nginx/certs/placeradar.lvh.me.key \
-out infrastructure/nginx/certs/placeradar.lvh.me.crt \
-subj "/CN=placeradar.lvh.me" \
-addext "subjectAltName=DNS:placeradar.lvh.me,DNS:localhost"To trust the certificate (optional):
- Firefox:
about:preferences#privacy→ View Certificates → Authorities → Import - Chrome/System: Add to OS trust store
- Alternative: Use mkcert for locally-trusted certificates
© 2025 PlaceRadar. All rights reserved.
