## SHINY — Scheduling and operations for Japanese welfare providers, on the web and in the hand. - URL: https://www.naveentehrpariya.dev/projects/shiny-soms - Industry: Welfare SaaS - Client type: Product Company - Country: Japan - Role: Full Stack Product Engineer - Duration: Ongoing - Status: Live - Year: 2026 A multi-tenant platform for Japanese non-profits running disability services, mobility transport, recreation and daycare. Bookings, staff rostering, vehicles, venues, attendance, incidents, daily contact books, compliance documents, live trip tracking and subscription billing sit in one system, with a web dashboard for coordinators and an Expo app for the staff, guardians and participants who are out in the field. ### Problem Welfare providers were running the day on paper and phone calls: a booking agreed by phone, a driver assigned on a whiteboard, a contact book filled in by hand and handed over at pickup. Nothing reconciled, guardians had no way to know whether the van was coming, and the compliance record only existed if someone remembered to write it down. ### Objectives - Run bookings, staffing, vehicles and venues from one schedule - Give guardians and field staff a phone app, not a cut-down website - Let a guardian see the van is on its way without exposing a staff member's movements - Bill in yen through Stripe with compliant Japanese invoices - Ship the whole product in English and Japanese, not English with a translation bolted on ### Solution One Express and Prisma API over PostgreSQL serves three clients: a React dashboard for admins and coordinators, and an Expo app for staff, guardians and participants. Roles are ranked, so an actor can never act on someone who outranks them. Live position is stored on the assignment row and nowhere else, consent is checked twice before a fix is kept, and every guardian-reachable booking read goes through one relation that deliberately omits the live coordinates. Stripe handles hosted checkout in yen, including konbini and bank transfer, and a single daily cron does the reminders, the sweeps and the weekly digest. ### Features - Bookings with approval, staffing, waitlist auto-promotion and cancellation rules - Live trip tracking with two-step consent and guardian-side coordinate blurring - Daily contact books (連絡帳), attendance and incident reporting - Per-organisation map provider — OSM by default, Google on the org's own key - Stripe subscriptions in JPY with card, konbini and bank transfer - Compliance documents on presigned, private object storage - Full English and Japanese interface across web and mobile ### Stack - Frontend: React 19, Vite, Tailwind CSS - Mobile: Expo SDK 54, React Native, expo-router - Backend: Node.js, Express, Prisma - Database: PostgreSQL - Cloud: Vercel, Backblaze B2, node-cron - Integrations: Stripe, Google Maps, OpenStreetMap, Expo Push ### Engineering challenges ### A guardian should see the van, not track the driver Problem: Guardians need to know their transport is on its way. Staff need not to be followed around all day. Those two requirements pull in opposite directions, and getting it wrong turns a helpful feature into surveillance of an employee. Approach: There is no ping-history table at all — position lives on the assignment row and is wiped when the trip ends, leaving a single 'ended here' point rather than a trail. Two consents are required before any fix is stored, and a guardian's view is snapped to a roughly 110 metre grid while dispatch keeps the exact fix. A source-level test asserts that every guardian-reachable booking read uses the relation that omits the live pair, because the blur only means anything if no path skips it. ### A confidently wrong pin sends a driver to the wrong city Problem: Free geocoding matches a whole address string or nothing, so real Japanese and Indian addresses often returned no result. The instinct is to retry with looser fragments, but a bare street name resolves to a different city entirely. Approach: Lookups are anchored to the organisation's own city and country, capped at two or three candidates with a deliberate gap, and never fall back to a bare fragment. No pin beats a wrong pin, so the mobile navigation screen degrades to the address and an 'open in Maps' action instead of showing a plausible but wrong marker. ### Maps that crashed the Android build with no way to catch it Problem: The native maps library hard-crashes a standalone Android build without a Maps key in the manifest, and a native crash cannot be caught by a React error boundary — the app just dies. Approach: Maps moved to Leaflet inside a WebView, which removes the native dependency entirely and let map provider become a per-organisation setting: OpenStreetMap by default so a new org costs nothing, Google only when an org supplies its own key and the platform verifies it before saving. ### Outcome - Bookings, staffing, vehicles and compliance records moved off paper into one schedule - Guardians can see a trip is under way without a staff movement trail ever being stored - New organisations onboard with zero map configuration and zero map cost - Billing, invoices and consumption tax handled in-product rather than by hand ### What it taught The privacy model was the architecture, not a setting on top of it. Deciding early that no movement history would exist made every later feature simpler, because there was never a table to accidentally expose. Case study: https://www.naveentehrpariya.dev/projects/shiny-soms More work: https://www.naveentehrpariya.dev/llms.txt