Points of Interest
Collaborative hotspot mapping through anonymous crowdsourcing

Points of Interest started as a proof-of-concept application for mapping nearby activity through anonymous crowd submissions.
The goal was not simply to let users drop markers on a map. The real goal was to transform many small, anonymous location signals into an aggregated live view of local activity.
The project explores how people can report nearby points of interest without creating accounts, while the system turns those reports into heatmaps, activity feeds, contributor statistics, and danger-zone style hotspot indicators.
Points of Interest is designed as a real-time geospatial feedback loop and combines Symfony, React, TypeScript, Leaflet, OpenStreetMap, Mercure, Doctrine, PostgreSQL/PostGIS, anonymous participation, real-time updates, and geospatial interface design.
The Problem
The core problem behind Points of Interest was not displaying a map. The real problem was making local activity visible without forcing users into a heavy reporting workflow.
Most mapping applications are built around fixed places:
But many real-world signals are temporary.
A place can become active for a short period. A zone can become crowded. A location can become relevant because multiple people report it around the same time.
That type of information is not static. It is collective, recent, and local.
Points of Interest explores a simple question:
Can anonymous nearby signals be transformed into a useful real-time map of local activity?
That question creates several technical challenges:
How do users submit a signal quickly?
How do we avoid requiring authentication?
How do we prevent spam or abusive submissions?
How do we validate that a signal is actually near the user?
How do we aggregate raw points into useful hotspot data?
How do we update every connected client in real time?
How do we keep the interface understandable on top of a dense map?A basic marker map would not be enough.
The system needed to collect signals, validate them, store them, aggregate them, stream updates, and render them as a clear geospatial interface.
That turned Points of Interest from a simple map demo into a full-stack real-time mapping system.
Product Direction
Points of Interest started with a simple idea:
Let people anonymously report nearby activity and visualize the result as a live heatmap.
That idea quickly became more interesting because the product needed to balance participation, privacy, and signal quality.
A useful version of the system needed to:
-
detect or request the user’s current location;
-
allow the user to submit a nearby point from the map;
-
validate that the submitted point is close enough to the user;
-
identify contributors without accounts;
-
rate-limit submissions per anonymous client;
-
store raw signals with timestamps;
-
aggregate recent signals into density cells;
-
expose snapshots through an API;
-
broadcast live updates to connected clients;
-
display hotspots, recent activity, and contributor statistics;
-
keep the interface usable without requiring authentication.
The product direction was intentionally lightweight.
The user should not need to create an account, fill a long form, or understand a complex reporting model. They should open the app, allow location access, tap the map, confirm the signal, and immediately see the shared map update.
System Architecture
Points of Interest is organized around five main layers:
Each layer has a clear responsibility.
The client layer handles interaction and visualization. The API layer receives and exposes signal data. The validation layer protects the quality of submissions. The persistence layer stores signals. The real-time layer keeps all clients synchronized.
The system follows this flow:
User action
-> map click
-> confirmation dialog
-> API submission
-> anonymous client identification
-> rate limit check
-> proximity validation
-> signal persistence
-> snapshot rebuild
-> Mercure broadcast
-> client heatmap updateThat structure keeps the project understandable while still covering the important parts of a real-time collaborative application.
Backend API Layer
The backend is a Symfony API responsible for receiving signals, returning snapshots, and coordinating live updates.
The API exposes one central resource:
/api/signalsIt supports two main operations:
GET /api/signals -> return a recent signal snapshot
POST /api/signals -> submit a new signalThe controller is intentionally thin. It delegates business logic to dedicated services:
This is the right design choice.
The controller should not know how to hash a user, validate geospatial distance, aggregate density cells, or publish real-time updates. It should only translate HTTP requests into application actions.
Snapshot Endpoint
The GET /api/signals endpoint returns a snapshot of recent activity.
The snapshot includes:
clientKey
points
density
latestByUser
totals
updatedAtThis gives the frontend everything it needs to render the map, heat layer, contributor count, recent activity, and user-specific state.
The snapshot model is useful because the frontend does not need to reconstruct the whole domain from raw database rows. It receives a prepared view of the current activity state.
Submission Endpoint
The POST /api/signals endpoint accepts a signal payload containing:
signalLocation
userLocationThat distinction matters.
The user’s current location is used to validate proximity. The signal location is the point they want to report. Those two coordinates may be the same, but they are not always the same.
For example, a user may be standing near a place and submit a signal slightly ahead of them on the map.
The backend checks whether that submitted point is within the allowed range before storing it.
Domain Model
At the center of the backend is the Signal entity.
A signal represents one anonymous report submitted by a user.
The entity stores:
id
userKey
userLocation
signalLocation
createdAtThis model is small, but it captures the essential information.
The system does not need a user account, profile, password, session table, or social identity. It only needs an anonymous contributor key, the submitted point, the user’s approximate position at submission time, and the timestamp.
That makes the data model lightweight and focused.
Anonymous Participation Layer
One of the most important product decisions was to avoid authentication.
Users can participate without creating an account.
Instead of storing identity, the backend derives an anonymous client key from the request IP address.
This gives the application enough identity to support:
rate limiting
latest signal by contributor
contributor statistics
user-specific latest signalwithout forcing users to register.
It is a pragmatic privacy tradeoff for a proof of concept.
The system does not claim to provide perfect anonymity. IP-derived identifiers can still carry privacy implications, especially if logs or raw addresses are retained. But for this prototype, the design keeps the user experience frictionless while avoiding direct account-based identity.
Validation Layer
Crowdsourced systems become useless if submissions are too easy to abuse.
Points of Interest handles this with two backend protections:
rate limiting
proximity validationRate Limiting
Each anonymous client key is passed through Symfony’s rate limiter before a signal is accepted.
The goal is simple:
One person should not be able to flood the map with fake activity.
The rate limiter protects the system from repeated submissions while keeping the interaction model simple for normal users.
Proximity Validation
The backend also checks that the submitted signal is close enough to the user’s actual location.
That matters because the map should represent nearby reality, not arbitrary remote reporting.
The validation flow looks like this:
The distance is calculated using a haversine-style distance function.
That is exactly the kind of validation this product needs. It is simple, explainable, and good enough for checking whether a reported point is reasonably close to the contributor.
Snapshot and Aggregation Layer
Raw signals are useful for storage, but they are not enough for visualization.
The frontend needs a higher-level representation:
Where are the points?
Where is activity dense?
Who submitted recently?
How many contributors are active?
When was the map last updated?The snapshot builder turns recent signals into a compact response.
The density aggregation groups signals by rounded coordinates and increments an intensity counter per bucket.
That produces heatmap-ready cells:
lat
lng
intensityThis is a smart design for the prototype because the frontend can render density without doing expensive grouping work itself.
The backend owns aggregation. The frontend owns visualization.
Real-Time Layer
Points of Interest uses Mercure to broadcast live signal updates.
When a signal is stored, the backend dispatches a message. A message handler rebuilds the recent snapshot and publishes it to a Mercure topic.
Connected clients subscribe through Server-Sent Events.
This gives the application its collaborative feel.
A user submits a signal, and other connected users can see the heatmap update without refreshing the page.
That is the difference between a static map and a live crowd map.
Frontend Map Layer
The frontend is a React and Vite application built around a Leaflet map.
Its job is to turn the backend snapshot into an interactive geospatial interface.
The main UI pieces are:
map viewport
heatmap layer
user location marker
confirmation dialog
header overlay
sidebar panels
recent activity feed
hotspot list
status and error feedbackThe frontend flow looks like this:
The frontend keeps the complex parts separated into hooks.
That is a strong architectural choice because map rendering, live data, derived statistics, and UI state are all different concerns.
Leaflet Heatmap
The map layer uses Leaflet and leaflet.heat to render density cells as a heatmap.
The heatmap receives backend-generated density cells and normalizes intensity values before rendering them.
The client also supports map tile providers, including OpenStreetMap and Mapbox-style tiles.
The result is more useful than showing individual markers only.
Markers show events. Heatmaps show patterns.
That distinction is the heart of the project.
Interaction Design
The user interaction is intentionally minimal:
open the app
allow location access
view nearby activity
click on the map
confirm the signal
watch the heatmap updateThe application does not ask for a name, email, password, profile, or category.
That is the right call for this prototype.
The core experiment is about frictionless participation. Every extra field would make the system heavier and distract from the main idea.
Confirmation Before Submission
A map click does not immediately submit a signal.
It opens a confirmation dialog first.
That small interaction matters because map clicks can be accidental. Confirming the point gives the user a chance to verify the location before contributing to the shared heatmap.
Local Radius Filtering
The frontend focuses on nearby activity.
It filters visible density cells, points, and latest-user signals around the user’s current location.
That keeps the experience local instead of turning the map into a global dump of unrelated points.
For this prototype, locality is the product.
Data Flow
The complete application data flow looks like this:
This is the main engineering value of the project.
It turns individual reports into a shared, live spatial view.
API Contract
The API contract is intentionally compact.
A snapshot response contains:
interface Snapshot {
clientKey?: string;
points: Signal[];
density: SignalDensityCell[];
latestByUser: Signal[];
totals: {
points: number;
contributors: number;
};
updatedAt: string;
}A signal contains:
interface Signal {
id: string;
signalLocation: Point;
createdAt: string;
userKey: string;
}A density cell contains:
interface SignalDensityCell {
lat: number;
lng: number;
intensity: number;
}This shape is clean because it matches the UI directly.
The frontend does not need to understand database rows. It receives exactly the data needed for:
map points
heatmap cells
contributor count
recent activity
latest signal by user
last update timeInfrastructure Layer
The local infrastructure uses Docker Compose for the supporting services.
The backend is designed around:
Symfony 7
PHP 8.4
Doctrine ORM
PostgreSQL / PostGIS
Mercure
AdminerThe frontend is designed around:
React
TypeScript
Vite
Leaflet
leaflet.heat
Zustand
i18next
Sonner
Tailwind-style UI componentsThe infrastructure can be summarized like this:
This stack fits the project well.
Symfony handles backend structure, validation, messaging, and persistence. React handles interactive UI. Leaflet handles the geospatial interface. Mercure handles real-time streaming. PostgreSQL/PostGIS gives the backend a geospatial-ready storage foundation.
Quality and CI
The repository includes quality checks for both backend and frontend.
The backend quality workflow runs:
PHPStan
ECS coding standards
PHPUnitThe frontend quality workflow runs:
ESLint
Prettier
TypeScript type checkingThat matters because this is not just a throwaway map demo.
The project has enough structure to be maintained, tested, and extended.
The important decision is separating backend and frontend checks.
Server changes trigger PHP workflows. Client changes trigger Node workflows. That keeps CI focused and cheaper to run.
Key Engineering Decisions
Build the product around live snapshots
The project does not stream individual low-level database events to the frontend.
Instead, it streams updated snapshots.
That is a good choice for this prototype because the UI wants the current state of the map, not a complex event log.
A snapshot gives the frontend a simple truth:
Here are the current points.
Here is the current density.
Here are the current contributors.
Here is the latest update time.Keep users anonymous
The application avoids authentication completely.
That lowers friction and makes the prototype easier to use.
The anonymous client key still gives the system enough structure for rate limiting and contributor statistics, but without introducing account management.
That is the right tradeoff for a crowdsourced local signal experiment.
Validate proximity on the backend
The frontend can guide the user, but the backend must enforce the rule.
Points of Interest checks the distance between the user location and the submitted signal location on the server.
That prevents the client from becoming the source of truth.
Never trust the browser for this kind of validation. That would be dumb.
Separate submission from visualization
Submission and visualization are separate flows.
A user submits one signal. The system rebuilds the snapshot. Every client receives the updated view.
That keeps the mental model simple:
write signal
rebuild state
broadcast state
render stateUse heatmaps instead of only markers
Individual markers are useful, but they do not communicate density well.
Heatmaps reveal patterns.
For a crowdsourced hotspot system, density is the product. The heatmap is not decoration. It is the main interface.
Use dedicated frontend hooks
The React code separates concerns through hooks:
useUserLocation
useHotspotFeed
useLeafletHeatmap
useFeedDerivations
useAppStoreThat keeps the app easier to reason about.
Map lifecycle, Mercure subscriptions, derived hotspot lists, geolocation, and UI state should not all live in one giant component.
Use Mercure for real-time updates
Mercure is a strong fit for this use case because the frontend only needs server-to-client updates.
The client does not need a full bidirectional WebSocket protocol. Server-Sent Events are enough.
That makes the real-time layer simpler.
Keep the API contract UI-friendly
The API returns density cells, latest signals by user, totals, and updated timestamps directly.
That keeps frontend rendering straightforward.
The backend prepares the data. The frontend presents it.
What I Learned
The biggest lesson from Points of Interest is that real-time mapping is not mainly about maps.
It is about trust, latency, aggregation, and interaction design.
A map is only the surface. Underneath it, the system needs to decide which signals are valid, how often a user can submit, how points become density, how updates reach clients, and how the interface avoids overwhelming the user.
I also learned that anonymity changes the architecture.
Without accounts, the system still needs a way to distinguish contributors, enforce limits, and show “my latest signal.” The anonymous client key solves enough of that problem for a prototype while keeping participation lightweight.
The project also showed that raw points are not the final product.
A list of coordinates is just data. A heatmap is interpretation. Density cells, contributor totals, recent activity, and danger-zone panels turn raw submissions into something a user can understand quickly.
Finally, the project reinforced a simple full-stack principle:
The backend should protect the data. The frontend should make the data feel alive.
Points of Interest does both.
Conclusion
Points of Interest is a full-stack proof of concept for collaborative hotspot mapping through anonymous crowdsourcing.
Its main value is not that users can click on a map. The value is the full loop around that action: anonymous identification, proximity validation, rate limiting, signal persistence, snapshot aggregation, real-time broadcasting, and heatmap visualization.
The system turns small individual reports into a shared live view of local activity.
That is the core idea behind Points of Interest:
Make nearby crowd activity visible without accounts, friction, or heavy reporting workflows.