Safinity is a mobile-first event safety app for large venues, festivals and conferences. It combines event navigation, friend location awareness, realtime notifications, SOS flows, ticket linking and crowd-density insights into one Expo app backed by a NestJS API.
The goal is simple: help people enjoy crowded events without losing context, losing friends, or losing time when something urgent happens.
Safinity is built around the live event experience:
- Attendees can sign in with Clerk and are automatically synced into the app database.
- Users can link tickets and access event-specific pages.
- Events, activities, tickets, maps, points of interest and friends come from PostgreSQL/PostGIS.
- Users can favourite activities and see them in a dedicated favourites calendar.
- Friends can be added through search, requests or QR code.
- Realtime notifications keep users updated about friend requests, SOS events and event alerts.
- SOS requests notify staff and friends attending the same active event.
- The map shows event points, stages, friend/user location and a crowd-density heatmap powered by sensors.
- Mapbox static maps are requested by the backend and delivered to the app.
Safinity-app/
├── backend/ # NestJS API
│ ├── prisma/
│ │ └── schema.prisma # PostgreSQL/PostGIS schema used by Prisma
│ ├── src/
│ │ ├── alerts/ # SOS/event alert handling
│ │ ├── auth/ # Clerk token validation and user sync
│ │ ├── common/ # Shared filters and helpers
│ │ ├── events/ # Events, activities, maps, favourites and tickets
│ │ ├── friends/ # Friend search, QR add, requests and buzz flow
│ │ ├── notifications/ # Notifications API and WebSocket realtime layer
│ │ ├── prisma/ # Prisma service
│ │ ├── sensors/ # Crowd-density sensors and ingestion logic
│ │ ├── sos/ # SOS request flow
│ │ ├── tickets/ # User ticket linking
│ │ └── users/ # Profile, roles and account data
│ ├── Dockerfile
│ └── package.json
│
├── frontend/ # Expo / React Native app
│ ├── app/ # expo-router screens and routes
│ │ ├── (tabs)/ # Home, map, calendar, friends, profile...
│ │ ├── activity-details/ # Activity details route
│ │ ├── event-details/ # Event details route
│ │ ├── sos.tsx # SOS flow
│ │ └── qrcode-scan.tsx # QR friend scanner
│ ├── assets/ # Images, icons, logos and fonts
│ ├── components/ # Reusable UI components
│ ├── constants/ # Theme and design tokens
│ ├── context/ # App-wide state providers
│ ├── data/ # Static fallback data
│ ├── utils/ # API clients and helpers
│ └── package.json
│
├── backoffice/ # Admin/backoffice frontend workspace
├── db/
│ └── init.sql # Local database bootstrap and seed data
├── docker-compose.yml # Local PostGIS database + NestJS API
├── readme/ # README images/mockups
└── README.md
| Area | Tools |
|---|---|
| Mobile app | Expo SDK 54, React Native, expo-router, TypeScript |
| Styling | styled-components, shared theme tokens |
| Auth | Clerk, JWT validation on the backend |
| API | NestJS, Prisma |
| Database | PostgreSQL with PostGIS |
| Realtime | WebSocket notifications |
| Maps | Mapbox Static Images API |
| Dev runtime | Docker Compose |
| Deploy target | Render backend + Supabase Postgres/PostGIS |
git clone <repo-url>
cd Safinity-appInstall frontend dependencies:
cd frontend
npm install
cd ..Install backend dependencies only if you plan to run it outside Docker:
cd backend
npm install
cd ..Create the files described in Environment Variables. The short version is:
- root
.envfor Docker Compose; backend/.envfor NestJS;frontend/.envfor Expo.
docker compose up -d --buildCheck the API:
curl -i http://localhost:3000/healthcd frontend
npm run startThen open it with:
- Expo Go on a physical phone;
- iOS Simulator;
- Android Emulator.
Never commit real secrets. The values below are examples.
Used by docker-compose.yml.
DB_NAME=safinity
DB_USER=safinity
DB_PASSWORD=<local-db-password>
DB_HOST=database
DB_PORT=5432
API_PORT=3000
DATABASE_URL=<local-docker-postgres-url>Inside Docker, DB_HOST must be database because that is the Compose service name.
Used by the NestJS API.
PORT=3000
DATABASE_URL=<postgres-connection-url>
CLERK_SECRET_KEY=sk_test_xxxxxxxxx
MAPBOX_TOKEN=pk.xxxxxxxxx
SUPABASE_URL=https://<project-ref>.supabase.co
SUPABASE_SERVICE_ROLE_KEY=<supabase-service-role-key>
SUPABASE_PROFILE_BUCKET=safinity
SENSOR_WEBHOOK_SECRET=change_me
ENABLE_SWAGGER=trueUseful notes:
CLERK_SECRET_KEYcomes from Clerk Dashboard -> API keys -> Secret key.MAPBOX_TOKENis used by the backend to request static map images.SUPABASE_SERVICE_ROLE_KEYis used only by the backend to upload profile photos to Supabase Storage.SUPABASE_PROFILE_BUCKETdefaults tosafinitywhen omitted.SENSOR_WEBHOOK_SECRETprotects sensor/webhook ingestion.- Set
ENABLE_SWAGGER=falsein deployed environments if you do not want/apiexposed.
Used by Expo. Public app variables need the EXPO_PUBLIC_ prefix.
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_xxxxxxxxx
EXPO_PUBLIC_API_URL=https://safinity-app.onrender.comFor local backend testing, temporarily override the value:
EXPO_PUBLIC_API_URL=http://localhost:3000If EXPO_PUBLIC_API_URL is missing, the mobile app defaults to the Render backend at https://safinity-app.onrender.com.
From the project root:
docker compose up -d --buildThis starts:
safinity-db: PostgreSQL with PostGIS;safinity-api: NestJS API on port3000.
Useful checks:
docker ps
docker logs -f safinity-api
docker logs -f safinity-db
curl -i http://localhost:3000/healthcd frontend
npm run startOther useful commands:
npm run ios
npm run android
npm run web
npm run lint
npm run format:checkUse this when you want the database in Docker but the API running directly on your machine.
Start only the database:
docker compose up -d databasePoint backend/.env to localhost:
DATABASE_URL=<local-postgres-url>Run the API:
cd backend
npm install
npx prisma generate
npm run start:devThe API should answer at:
http://localhost:3000
The database is PostgreSQL with PostGIS. It stores:
- Clerk-synced users;
- events, event images and event metadata;
- activities and favourite activities;
- ticket master codes and linked user tickets;
- friendships and friend request states;
- notifications and per-user read state;
- SOS requests and generated alerts;
- points of interest, stages, sensors and user locations.
Open a local database shell:
docker exec -it safinity-db psql -U safinity -d safinityUseful queries:
select id, name, username, email, role from users order by name;
select id, name, status, start_date, end_date from event order by id;
select user_id, event_id, ticket_code from user_tickets order by linked_at desc;
select id, title, category, event_id, time from notifications order by time desc;The backend can be deployed to Render using the backend/Dockerfile.
For Render + Supabase, configure at least:
DATABASE_URL=<supabase-postgres-url-with-ssl>
CLERK_SECRET_KEY=sk_test_or_sk_live_xxxxxxxxx
MAPBOX_TOKEN=pk.xxxxxxxxx
SUPABASE_URL=https://<project-ref>.supabase.co
SUPABASE_SERVICE_ROLE_KEY=<supabase-service-role-key>
SUPABASE_PROFILE_BUCKET=safinity
ENABLE_SWAGGER=false
NODE_ENV=productionImportant details:
- Do not put quotes around
DATABASE_URLin Render. - Do not use
${DB_PASSWORD}inside Render'sDATABASE_URL; put the final URL value directly. - If using the Supabase pooler, include the correct pooler username and SSL mode.
- Keep
CLERK_SECRET_KEYonly on the backend. - The mobile app should only receive
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY.
The deployed app currently targets:
https://safinity-app.onrender.com
Safinity uses GitHub Actions for CI and SonarCloud for code quality verification.
SonarCloud is configured through:
sonar-project.properties
.github/workflows/sonarcloud.yml
To enable it in a new fork/repository:
- Create/import the project in SonarCloud.
- Confirm the project key and organization match
sonar-project.properties. - Create a GitHub repository secret named
SONAR_TOKEN. - Push to
mainor open a pull request againstmain.
The SonarCloud workflow installs project dependencies, runs backend Jest coverage, then sends the analysis to SonarCloud. The Quality Gate badge at the top of this README becomes active after the first successful scan.
Before committing backend changes:
cd backend
npm run lint:check
npm test -- --runInBandBefore committing frontend changes:
cd frontend
npm run lint
npm run format:checkGeneral sanity check:
git status
git diff --statUse this list after changing authentication, events, friends, maps or notifications:
- Login with email/password.
- Login with Google.
- Open profile and confirm user data loads from the API.
- Link a ticket from event details or wallet.
- Open event details and navigate to the correct map/calendar.
- Favourite an activity and confirm it appears in Favourites activities.
- Search for a friend from database users.
- Send, accept and remove a friend request.
- Add a friend through QR code.
- Open the map and confirm points of interest, stages and user location render.
- Toggle the heatmap filter.
- Send an SOS request and confirm realtime notifications.
- Logout and confirm protected pages no longer use the previous session.
Docker Desktop is not running or is still starting.
docker infoIf the server section fails, open Docker Desktop and wait until it is ready.
Check logs:
docker logs --tail 150 safinity-apiCommon causes:
- missing
CLERK_SECRET_KEY; - invalid
DATABASE_URL; - database container is not healthy;
- port
3000is already in use.
If Prisma shows P1000, the database credentials are wrong.
If Prisma says the URL must start with the PostgreSQL protocol, the value in Render probably includes quotes, DATABASE_URL=, spaces, or an unresolved variable.
The Render value should start directly with the database protocol, with no quotes or key name pasted into the value field.
The app uses Expo SDK 54. The installed Expo Go version must support that SDK. If the physical phone cannot install a compatible Expo Go version, use an emulator/simulator or build a development/native build deliberately.
Check the API locally:
curl -i http://localhost:3000/healthThen try from the phone browser:
http://<your-laptop-ip>:3000/health
If the phone cannot open it:
- put phone and laptop on the same network;
- avoid networks that isolate devices, such as some university networks;
- try a phone hotspot;
- check macOS firewall settings;
- confirm Docker exposes
0.0.0.0:3000->3000/tcp.
Check:
- frontend has
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY; - backend has
CLERK_SECRET_KEY; - frontend
EXPO_PUBLIC_API_URLpoints to a reachable API; - backend logs do not show 401/invalid token errors;
- Expo was restarted after env changes.
Restart Expo with cache clear:
cd frontend
npx expo start -cGrant camera permission to Expo Go or the simulator:
iOS Settings -> Expo Go -> Camera
Then reload the app.
Check:
MAPBOX_TOKENexists inbackend/.envor Render env;- the token has access to Mapbox Static Images API;
- the event has valid coordinates;
- backend logs do not show Mapbox request errors.

