GroundShare is a Hebrew (RTL), mobile-first PWA + native iOS/Android app (Capacitor) for urban-planning transparency in Israel. Users view a map of their neighborhood, see official planning data (construction permits, road work, city plans from Tel Aviv GIS / iplan / mavat), and crowdsource community reports (noise, blockages, etc.). Features include address comparison with an AI summary, favorites, reviews, voting, neighborhood clustering, push notifications, and a gamification system (XP/levels/streaks).
Production is live on Azure (Israel Central):
- Custom domain:
https://groundshare.app(DNS via Cloudflare) - API:
https://api.groundshare.app(Cloudflare → Azure App Serviceapp-groundshare-api) — same-site with the web app so the httpOnly refresh cookie works - Frontend: Azure App Service (Node host serving the Vite
dist/bundle) - Database: Azure SQL (
sql-groundshare-il.database.windows.net) - Secrets: Azure Key Vault via Managed Identity — no secrets in env vars
.github/ CI/CD workflows (ci, cd, codeql, cluster-daily, relevance-daily)
docs/ Setup, deploy, security, AI context, feature inventory, plans
src/
01-database/ GroundShareDB.sql — complete schema + stored procedures (single source of truth)
ResetData.sql — wipes user data while keeping schema + lookups
02-server/ C# ASP.NET Core 8 Web API
Controllers/ 22 controllers (Auth, Users, Events, GeoMaps/GeoInfrastructure/GeoPlanning,
Locations, Favorites, Ai, Comments, Comparisons, Contact, Files, Health,
Notifications, Clustering, Relevance, UserReviews, Cities,
Streets, EventTypes, PlanStatus)
DAL/ Data access — pure ADO.NET, NO Entity Framework. All DB calls use
SqlConnection + SqlCommand, mostly via stored procedures (sp_*).
DAL/Interfaces/ holds the DAL contracts used for DI.
BL/ DTOs + domain helpers (LoginRequest, CreateEventRequest, User,
IsraeliTM, KMeans, ScoringModels, TlvAddressService, ...)
Services/ Business logic (AuthService, EventService, ComparisonsService,
GisFeatureService, ContentSafetyService, HebrewBlocklist,
storage, FCM, email, audit) + 3 background/hosted services
Domain/ Plain domain types
Exceptions/ AppExceptions — typed exceptions mapped to HTTP status in middleware
Extensions/ DI + pipeline registration extension methods (Program.cs is thin)
Filters/ ValidationFilter (FluentValidation → ApiResponse envelope)
Middleware/ Exception handling, ApiResponse envelope, security headers, request logging
Logging/ LogRedactionEnricher (PII masking for Serilog)
Options/ Strongly-typed config classes (Jwt, GoogleApi, Storage, Resend, ContentSafety)
Validators/ FluentValidation rules for request DTOs
03-client/ React 19 + TypeScript + Vite 7
src/app/
features/ Feature-folder screens. One folder per surface:
auth/ main/ address-search/ nearby/ compare/ create-event/
favorites/ notifications/ personal/ settings/ dictionary/
Each can hold sub-components + a lib/ for pure helpers (+ __tests__).
components/shared/ Reusable UI used across features (BottomNavBar, TopSearchBar,
MapLayout, DrawerMenu, map overlays, appIcons.tsx, ...)
onboarding/ First-time questions + replayable in-app tour (TourContext/Overlay)
context/ AuthContext (JWT + user state), GeoContext (GPS watchers),
MapContext (map config + overlays)
hooks/ useGeolocation, useGoogleMapsKey, useSwipeBack, useAsync,
useHomeNavigation, useNotificationFeed, usePendingConfirmations, ...
services/ api/ (per-domain modules + client.ts transport), addressAutocomplete.ts,
pushNotifications.ts, authErrors.ts. (api.ts is a thin re-export shim.)
types/ Centralized backend-shape types (one file per domain, Snake_Case fields)
utils/ eventDisplay, relativeTime, serverDate, ...
constants/ notifications.ts, onboardingOptions.ts
routes.ts React Router 7 — createBrowserRouter with ProtectedRoute/GuestRoute guards
src/styles/ tailwind.css, theme.css (CSS variables), fonts.css (Heebo, Poppins)
android/ ios/ Capacitor 8 native shells
04-tests/ GroundShareAPI.Tests — xUnit unit tests for backend pure logic
(IsraeliTM, KMeans, NeighborhoodPolygon, HebrewBlocklist,
AdminTokenValidator, FluentValidation validators). Run in CI.
tools/ Pre-commit hook for gitleaks + installer
# Database: Run src/01-database/GroundShareDB.sql in SSMS against local SQL Express.
# DB starts empty — register your first user through the app's Register screen.
# To reset between tests: run src/01-database/ResetData.sql
# Backend (port 5227)
cd src/02-server
dotnet run
# Frontend (port 5173)
cd src/03-client
npm install
npm run devBackend requires appsettings.Development.json (gitignored) with local SQL connection string, JWT key, and Google Maps API key. See SETUP.md for details.
The backend does NOT use Entity Framework. All database access is manual ADO.NET through a base class DBServices:
// Every DAL class inherits DBServices and uses this pattern:
using SqlConnection con = Connect();
var p = new Dictionary<string, object> { { "@Param", value } };
SqlCommand cmd = CreateSP("sp_StoredProcName", con, p);
return ReadAll(cmd); // Returns List<Dictionary<string, object>>- Stored procedures use
sp_prefix (e.g.,sp_CreateEvent,sp_RegisterUser) - Database columns use Snake_Case:
User_ID,Event_ID,City_Name,Start_Date - Most DAL methods return
Dictionary<string, object>— not typed models - Some DAL methods use raw SQL instead of SPs (inconsistent but functional)
- New endpoints: Controller → Service (if logic is non-trivial) → DAL class → stored procedure
Non-trivial logic lives in Services/, not controllers. Examples already extracted:
ComparisonsService— resolve → score → classify → persist → AI summary (was inComparisonsController)GisFeatureService— shared GIS fetch + ArcGIS parsing + cross-layer disruption dedup + geometry math + geocode backfill, used by the three Geo* controllers and the clustering job (so the dedup logic is no longer copy-pasted)EventService— event creation + the fire-and-forget FCM fan-outAuthService— token issuance/rotation helpers
DI is registered via extension methods in Extensions/ (AddApplicationServices, AddDataAccessLayer, AddBackgroundJobs, AddAppHttpClients, AddConfiguredCors, AddAppRateLimiting, etc.) — keep Program.cs thin and add new registrations to the matching extension.
Throw typed exceptions from Exceptions/AppExceptions.cs (e.g. NotFoundException, ConflictException); ExceptionHandlingMiddleware maps them to the uniform ApiResponse<T> envelope with the right status code. Don't return raw error strings from controllers. SQL unique-constraint collisions surface as specific patterns (EMAIL_EXISTS, error numbers 2601/2627) — catch those where relevant.
- All components are functional with hooks (useState, useEffect, useCallback, useRef)
- Named exports for components:
export function MainScreen() { ... } - Feature folders: screens live under
app/features/<feature>/; pure helpers go in<feature>/lib/. Cross-feature UI lives inapp/components/shared/. - Routing: React Router 7 with
createBrowserRouter(object form,Component:) inroutes.ts;RouterProvidermounted inApp.tsx. Import fromreact-router(notreact-router-dom). - Auth state:
useAuth()from AuthContext —user,login,logout,isAuthenticated - Geo state:
useGeolocation()(thin wrapper over GeoContext) — dual coarse + high-accuracy watchers - Map state:
useMap()from MapContext — screens setconfigandoverlays, MapLayout renders them - API calls: per-domain modules under
services/api/*over a singlerequest<T>transport inservices/api/client.ts, with typedApiErrorand single-flight 401 refresh - Navigation:
useNavigate()with state passing vialocation.state
- Access token lives in JS memory only, on both web and native — never in localStorage.
- Web: refresh token is an httpOnly
gs_refreshcookie (SameSite=Lax, Secure, path/api/auth); the cookie path requires anX-CSRFheader. - Native (
X-Client-Platform: native): refresh token returned in the body and kept in sandboxed localStorage. - Refresh is single-flight in
services/api/client.ts; the server does single-winner rotation with reuse detection (sp_RotateRefreshToken).
- Primary: Tailwind CSS 4 classes + inline
style={{}}for custom colors/gradients/shadows - Color palette:
#063c55(dark teal primary),#7ECFE5(light cyan),#f68d1e(orange accent),#f3fbfd(light bg) - Fonts: Heebo (Hebrew text), Poppins (Latin/numbers)
- RTL:
dir="rtl"on root divs. Usedir="ltr"wrapper when flex direction needs reversing - Icons:
lucide-react+@tabler/icons-react, centralized incomponents/shared/appIcons.tsx(single source of truth — swap a glyph there and every call site updates). Compare-specific Figma SVGs infeatures/compare/icons.tsx. Seesrc/03-client/docs/icon-map.md. No MUI / Radix (removed). - Responsive: Mobile-first; desktop shows a phone-frame preview (412×915px)
- Google Maps via
@vis.gl/react-google-mapswithAdvancedMarker - API key fetched from server at runtime via
GET /api/geo/maps-config(never hardcoded in client) - MapContext overlay system: screens push markers/circles, MapLayout renders them
- Persistent map:
MapLayoutstays mounted across the map routes (/main,/nearby-reports,/address-search,/questions) - Building footprints (
BuildingsOverlay, TLV layer 513) + house-number labels (AddressNumbersOverlay, layer 527) + neighborhood polygons + TLV satellite tiles are all mounted inMapLayout - Smooth panning via sequence counter in
panTo() - Default center: Tel Aviv (32.0853, 34.7818)
- Base URL (dev):
http://localhost:5227/api. Production:https://api.groundshare.app/api. - Auth: JWT Bearer (access token, 15-min) in the
Authorizationheader; access token held in memory (see above) - Token refresh: automatic single-flight on 401 in
services/api/client.ts - CORS: localhost:5173 / :4173 in dev; native Capacitor origins + the web origin in prod
POST /api/auth/register, /login, /google, /refresh, /logout, /forgot-password, /reset-password
GET /api/users/me, /{id} PUT /api/users/onboarding, /home-address, /phone
GET /api/users/activity POST /api/users/missions/claim DELETE /api/users/me
POST /api/events Create event (fires FCM fan-out)
GET /api/events/nearby?lat=&lng=&radius= Nearby events (anonymous)
GET /api/events/location/{id} Events for a location (anonymous)
POST /api/events/{id}/vote Upvote/downvote
GET/POST /api/events/{id}/comments Comment thread
GET /api/events/my-pending-confirmations · POST /api/events/{id}/confirm-relevance
GET /api/geo/maps-config Get Maps API key (GeoMapsController)
GET /api/geo/geocode | /reverse | /autocomplete (GeoMapsController)
GET /api/geo/parcel | /buildings | /address-points (GeoInfrastructureController)
GET /api/geo/planning-status | /nearby-disruptions (GeoPlanningController)
POST /api/locations/resolve Resolve address → location_id (anonymous)
GET /api/locations/{id} | /{id}/stats | /{id}/plans
POST /api/comparisons/compare Score + AI summary
POST /api/comparisons Save GET /api/comparisons/history
POST /api/userreviews · PUT /api/userreviews/{id} · GET /api/userreviews/location/{id}
POST /api/favorites/toggle · GET /api/favorites · GET /api/favorites/check/{id}
GET /api/notifications, /feed · POST /notifications/toggle/{id}, /device · GPS sub endpoints
POST /api/ai/address-summary · POST /api/ai/ask
GET /api/clustering/neighborhoods · /diagnostics POST /clustering/run-now (admin)
The single
GeoControllerwas split into GeoMapsController (maps key, geocode, autocomplete, reverse), GeoInfrastructureController (parcel, buildings, address-points), and GeoPlanningController (planning-status, nearby-disruptions) — all still under the/api/georoute prefix.
| Layer | Convention | Examples |
|---|---|---|
| DB columns | Snake_Case | User_ID, City_Name, Start_Date |
| Stored procs | sp_ prefix | sp_CreateEvent, sp_GetUserById |
| C# classes | PascalCase | EventsController, EventsDAL, GisFeatureService |
| C# DTOs | PascalCase | CreateEventRequest, LoginRequest |
| TS components | PascalCase files | MainScreen.tsx, BottomNavBar.tsx |
| TS services/hooks | camelCase files | client.ts, useGeolocation.ts |
| API routes | lowercase | /api/events, /api/userreviews |
- All user-facing text is in Hebrew
- Event statuses: "קורה" (happening), "קרה" (happened), "עתיד לקרות" (upcoming)
- User roles: "שוכר", "בעל דירה", "משקיע", etc.
- City/street autocomplete uses data.gov.il CBS dataset (Hebrew names with trailing spaces — handled in code via
fn_NormalizeAddressPart) - Always test RTL layout when adding UI components
src/01-database/GroundShareDB.sql is the single source of truth for the entire schema.
- Never create migration files. All schema changes (new tables, new SPs, modified SPs, new indexes) go directly into
GroundShareDB.sql. - Every table uses
IF OBJECT_ID(...) IS NULL CREATE TABLE ...so the file is safe to re-run on existing databases. - Every SP uses
CREATE OR ALTER PROCEDURE— idempotent by design. - Every index uses
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = '...') CREATE INDEX .... ResetData.sqlwipes all user data but keeps schema andevent_typeseed rows. Update it whenever a new table with anIDENTITYcolumn is added.- All city/street write-path SPs must call
dbo.fn_NormalizeAddressPart()before insert/lookup. - All multi-statement SPs that touch more than one table must use
BEGIN TRY / BEGIN TRANSACTION / COMMIT / CATCH / ROLLBACK / THROW. - Refresh tokens are stored hashed (SHA-256 hex) — the hash is computed in C# before calling the SP; the SP receives only the hash. Rotation/reuse-detection runs through
sp_RotateRefreshToken(single-winner;Replaced_Byholds the successor's hash). - Address identity: coord-bearing resolves snap to the TLV
id_ktovet(layer 527) viaTlvAddressService; precedence isTlvAddressId → fine GeoKey → text(seedocs/ADDRESS_IDENTITY_PLAN.md).
The user runs all SQL himself in SSMS against PROD. Edit
GroundShareDB.sql/ResetData.sqland tell him exactly what to run — never execute SQL or connect to a database.
- Database: Add table/SP directly in
src/01-database/GroundShareDB.sql(user runs it in SSMS) - DAL: Create
XxxDAL.csinheritingDBServices, call SP withCreateSP(); add an interface inDAL/Interfaces/if it'll be injected - Service (if logic is non-trivial): add a
XxxService.csand register it in the matchingExtensions/*method - DTO: Add request/response models in
BL/; add a FluentValidation validator inValidators/ - Controller: Create
XxxController.cswith[Route("api/[controller]")](thin — delegates to the service/DAL) - Frontend API: Add a function in the right
services/api/<domain>.tsmodule and re-export fromservices/api/index.ts - Types: Add the backend shape to
app/types/(Snake_Case fields) - Screen: Create the screen under
app/features/<feature>/, wire up the API call - Route: Add to
routes.ts(withProtectedRoute/GuestRoutewrapper as needed) - PWA: If it's a live-data
/api/*endpoint, add it to the NetworkOnly bypass list invite.config.ts(or the SW will serve stale data across deploys)
- The backend uses ADO.NET, not EF Core — don't generate EF migrations or DbContext code
- API responses are often
Dictionary<string, object>— keys match DB column names (Snake_Case) - The
appsettings.jsonin the repo has placeholder values — real secrets are inappsettings.Development.json(gitignored) / Key Vault (prod) - Google Maps API key must come from server
/api/geo/maps-config— never embed in client code - The Geo* controllers fan out parallel async calls to multiple Tel Aviv GIS layers with 8–10s timeouts via
GisFeatureService— be careful changing the dedup/geometry logic there ags.iplan.gov.il(MAVAT) needs legacy TLS — use the named"iplan"HttpClient (relaxedCipherSuitesPolicy), not the default client; TA GIS uses the default client- Don't pass
outSR=4326on TA GIS geometry queries — fetch native EPSG:2039 and reproject withIsraeliTM.ToWgs84(which adds the Israel-1993→WGS84 datum shift) - Don't SW-cache
maps.googleapis.com/gstaticmap tiles (only the TLV satellite CacheFirst rule is intended); the/api/auth/*NetworkOnly rule must stay FIRST inruntimeCaching