Find safe gluten-free dining across Europe and the UK
SafeBite is an iOS application designed to help people with coeliac disease find verified gluten-free restaurants. With a unique three-tier trust scoring system, users can make informed decisions about where to eat safely.
- Interactive Map Discovery - Browse gluten-free restaurants on a map with location-based filtering
- Trust Score System - Transparent verification showing restaurant safety:
- Professional Score (0-40): Verified certifications, owner questionnaires, dietitian validation
- Community Score (0-35): Reviews from verified users weighted by credibility
- Freshness Score (0-25): Time-decay based on verification/review recency
- Restaurant Safety Profiles - Detailed info including dedicated kitchen, separate fryers, staff training, and certifications (Coeliac UK, AIC, DZG, GFCO, GFFP)
- Verified Reviewer System - Users pass a 10-question safety quiz to become trusted reviewers
- Saved Favorites - Bookmark restaurants with sorting and filtering options
- Safety Incident Reporting - Document and report cross-contamination incidents
- User Profiles - Track dietary needs and severity levels (coeliac, NCGS, wheat allergy, preference)
- Premium Subscription - Monthly/yearly tiers for enhanced features
- Multi-language Support - English, German, French, Italian, Spanish
- GDPR Compliance - Full data export, deletion, and consent management
- Language: Swift 5.9+
- UI Framework: SwiftUI
- Architecture: The Composable Architecture (TCA) v1.15+
- Data Persistence: SwiftData
- Backend: Firebase (Auth, Firestore, Crashlytics, Analytics)
- Maps: MapKit
- In-App Purchases: StoreKit 2
- Image Loading: Kingfisher
- iOS 17.0+ / macOS 14.0+
- Xcode 15+
- Swift 5.9+
- Google Places API Key - Get one from Google Cloud Console
- Firebase Project - Create a project at Firebase Console
-
Clone the repository
git clone https://github.com/Bogdan0708/Safebite.git cd Safebite -
Open in Xcode
cd SafeBite open Package.swift -
Configure Firebase
- Download
GoogleService-Info.plistfrom your Firebase project - Add it to the
SafeBitetarget in Xcode
- Download
-
Configure API Keys
- Copy
Config.xcconfig.templatetoConfig.xcconfig - Add your Google Places API key:
GOOGLE_PLACES_API_KEY = your-api-key-here
- Copy
-
Build and Run
- Select an iOS 17+ simulator or device
- Press
Cmd + Rto build and run
Deploy Firestore security rules and indexes:
cd SafeBite/scripts
bash setup.shSafeBite/
├── SafeBite/
│ ├── SafeBiteApp.swift # App entry point
│ ├── Features/ # TCA-based feature modules
│ │ ├── Auth/ # Authentication flows
│ │ ├── Map/ # Main map view
│ │ ├── Search/ # Restaurant search
│ │ ├── Saved/ # Favorites management
│ │ ├── Profile/ # User settings
│ │ ├── RestaurantDetail/ # Restaurant info
│ │ ├── Review/ # Review submission
│ │ ├── Subscription/ # Premium paywall
│ │ └── GDPR/ # Privacy settings
│ ├── Models/ # Data models
│ ├── Services/ # Business logic services
│ └── Resources/ # Localization & assets
├── SafeBiteTests/ # Unit tests
├── SafeBiteUITests/ # UI tests
├── scripts/ # Setup and deployment scripts
├── firestore.rules # Firestore security rules
└── firestore.indexes.json # Firestore indexes
SafeBite follows The Composable Architecture (TCA) pattern:
- Each feature is composed of a
Feature(reducer) and aView - State is managed predictably through actions and reducers
- Side effects are handled through the
Effecttype - Dependencies are injected for testability
Run the test suite:
cd SafeBite
swift testOr in Xcode: Cmd + U
SafeBite supports 5 languages:
- English (UK) - Primary
- German (de)
- French (fr)
- Italian (it)
- Spanish (es)
SafeBite is fully GDPR compliant:
- Consent required before app use (EU/UK requirement)
- Data export available as JSON
- Account deletion with cascading data cleanup
- Firestore data stored in EU region
- Health data (severity level) requires explicit consent
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is proprietary software. All rights reserved.
For questions or support, please open an issue on GitHub.
SafeBite - Eat safely, anywhere in Europe.
The active codebase is the web app in web/ with callable functions in
functions/. The Swift project under SafeBite/ is kept as a reference only.
- Node 22, npm 10
- Java 21+ (Firebase emulators)
npm ciin./,web/, andfunctions/- On WSL, keep the checkout on the Linux filesystem (for example
~/dev/AvaGF), not/mnt/c: Windows-mounted paths make emulator cold starts take over a minute.
npm run emu:start # terminal 1: Auth, Firestore, Functions emulators (project demo-safebite)
npm run emu:seed # terminal 2, once: creates ava@safebite.test / bogdan@safebite.test (members)
# and stranger@safebite.test (not a member); password pilot-password-1
npm --prefix web run dev # terminal 2: http://127.0.0.1:5173Discovery (the Discover tab) calls the
searchDestination/searchNearbyfunctions, which read thePLACES_API_KEYsecret. Locally the emulator readsfunctions/.secret.local(gitignored); theemu:*scripts create it fromfunctions/.secret.local.example(PLACES_API_KEY=fixture) when it is missing, which selects a fixture provider with twelve invented venues and the magic queries__empty__,__unavailable__,__quota__,__delayed__(3 s) and__slow__(25 s). To try real results locally, put a key restricted to Places API (New) in.secret.local— never commit it. Search is off untilconfig/discoveryexists ({ enabled: true, dailySearchCap: 50 }, written bynpm run emu:seed).
Emulator UI: http://127.0.0.1:4000
Records live under households/home/restaurants in the emulator; npm run emu:e2e clears them before each scenario via the emulator's REST API.
npm run typecheck # both packages
npm run test:unit # web unit tests (no emulator)
npm run emu:test # functions + Firestore rules tests (starts emulators)
npm run emu:e2e # Playwright browser tests (starts emulators, seeds, runs Vite)
npm run emu:e2e:stress # 29 browser scenarios × 3 repeats, retries disabled (flakiness gate)
npm --prefix web run build:check # compile-only build (no Firebase config needed)
npm --prefix web run build:e2e # builds the three synthetic bundles: dist-preview, dist-preview-v2, dist-boot-guard (fixtures in web/.env.preview, .env.preview-v2, .env.boot-guard)
npm --prefix web run e2e:boot-guard # compile-only bundle with demo values refuses to start (Chromium, no emulators)
npm --prefix web run e2e:preview # manifest, service worker, offline shell (Chromium, no emulators)
npm --prefix web run e2e:upgrade # same-origin release upgrades and the update prompt (Chromium, no emulators)
npm --prefix web run icons # re-render the PNG icon set from web/assets/safebite-mark.svgnpm run test:unit currently reports 270 tests.
- Local work targets the emulator-only project
demo-safebite. Nothing here deploys. - Membership (
users/{uid},households/{hid}) is written only with the Admin SDK; there is no sign-up. - Never reuse the legacy seed data from git history; its safety claims were invented.
npm --prefix web run build(used byfirebase deploy) refuses missing, blank, demo-, or legacy-project Firebase values; the resulting bundle also refuses to start against them.- Any
vite buildrefuses to run withNODE_ENVset to anything butproduction(including via.envfiles), even forbuild:check. A built bundle's startup guard keys on the__SAFEBITE_BUILD__marker fromvite.config.ts, not onimport.meta.env.PROD, soNODE_ENVcannot switch it off. - A misconfigured built bundle shows a plain "this build is misconfigured" screen, loads no Firebase code, unregisters every service worker, deletes every cache and leaves the old worker's control; a clean bundle registers the Workbox worker only after that check. A non-deployable build ships a self-destroying worker (no precache), so an installed worker that picks it up as an update never caches it and triggers a clean-up; the misconfiguration screen completes the purge (
npm --prefix web run e2e:upgradeproves both paths). - Updates are prompted, never forced: a new release shows a "new version ready" banner and only the tab whose Reload is tapped reloads; other open tabs get an "updated in another tab" banner and keep their typed input until they reload (
e2e:upgradeproves both). - The PWA icon set is generated, never hand-edited: change
web/assets/safebite-mark.svgand runnpm --prefix web run icons. - The three synthetic test bundles are built only from the committed
web/.env.<mode>fixtures:vite build --mode preview|preview-v2|boot-guardrefuses to run if an exportedVITE_*variable differs from the file, and every build writessafebite-build.json(mode, project id, source hash) that thee2e:*scripts verify, so a stale or wrong-mode dist is refused with thebuild:<mode>command to run. - Records are member-only and written only through Firestore transactions (online-only; a save is reported as saved only after the server accepted it). Restaurants carry a
versionthe rules require to increase by exactly one; claims are immutable (add/delete only); deleting a restaurant marks it, sweeps its claims, then removes it, and an interrupted deletion is resumed from the list. - Evidence dates are UTC calendar days stored at 00:00 UTC; a claim needs rechecking 12 months after it was checked unless it carries its own expiry. Same-day contradicting claims are shown as "Conflicting evidence". No numerical score anywhere.
- Discovery is server-side only: the Places key is a Cloud Functions secret, every callable checks membership first, the field mask asks for id, name, address, Maps link, business status and types only (Pro tier; no coordinates, phone, website or ratings), Town or area searches request
food in <query>, while Restaurant or venue name searches preserve the entered name; both make one request and the mapper keeps only restaurants, cafés, bakeries, bars and takeaways (no restaurant-only request filter), and nothing from a Places response is cached or stored — a record created from a result keeps only the place id; the member types the name and address. A deployed function whose secret is the fixture value refuses every search. - Search fails closed: a missing or disabled config/discovery document refuses every search; each household has a per-day cap counted before the provider is called (failed calls count too). Results carry the Google Maps logo and are shown in Google's order; a listing says nothing about gluten-free safety.