Context
Photos and video get cropped, re-shared and re-captioned until nobody can say where they came from. People who need to rely on footage need to know where it was captured, when, and in which direction, and that information has to survive the trip from the phone to whoever looks at it next.
Constraints
- Mobile connections in the field drop constantly.
- Location and bearing must survive upload and processing.
- Private events must stay private, even from other users of the app.
- Personal data must be erasable on request.
- Captured in the fieldGPS position and a continuous compass log recorded with the video
- Uploaded in chunksResumable session survives a dropped mobile connection
- Checksum verifiedSHA-256 of the assembled file matches the one sent by the phone
- Context burned into the frameFFmpeg adds compass overlay, coordinates and timestamp
- Published to the event mapVisibility checked against the event's permission model
My responsibility
Sole engineer: architecture, backend, the mobile app and the web portal.
Architecture
- Capture
- A React Native app records photos and video with GPS coordinates and a continuous compass log.
- Upload
- Resumable, chunked upload sessions with a SHA-256 checksum of the whole file verified when the chunks are assembled.
- Processing
- FFmpeg burns a compass overlay, coordinates and timestamp into every photo and video and generates thumbnails, asynchronously and off the request path.
- Identity and access
- Keycloak as the identity provider with PKCE for the web; the backend is an OAuth2 resource server. Phone verification by one-time code, stored only as a hash. One permission model for public and private events, member roles and access requests.
- Web portal
- A Next.js portal with a MapLibre map of media and events, built with TanStack Query.
Critical decisions
Log the bearing continuously
A video records the compass bearing for every moment of the recording, so a pan shows up in the data.
Put the context in the frame
Coordinates, bearing and time are burned into the media itself, so they survive being downloaded and re-shared.
Identity only from the token
The backend never trusts a user id sent by the client; every request is attributed from the verified token.
Keep the map out of every other page
The map bundle is split from all non-map routes, taking their initial JavaScript from 996 KB to 84 KB.
When things go wrong
| When | What happens |
|---|---|
| The connection drops mid-upload | The session resumes from the last received chunk. |
| The assembled file doesn't match the phone's checksum | The mismatch is caught at assembly, before the file is stored. |
| A client sends someone else's user id | It is ignored; identity comes from the token. |
| A user deletes their account | Database rows and stored media are removed together. |
Result
The proof of concept passed validation.
996 → 84 KB
Initial JavaScript on non-map routes after splitting the map bundle
Photo and video captures during the pilot
Stack
Java 21 · Spring Boot 3 · PostgreSQL · Flyway · FFmpeg · S3 · Keycloak · React Native · Next.js · MapLibre