A dual-sided mobile application for managing marina operations, built with React Native (Expo) and Supabase.
The Marina app serves two types of users:
- Customers — boat owners who schedule put-ins, manage reservations, track their boat's status, and communicate with marina staff
- Workers / Admins — marina staff who manage the slip board, approve boats, track daily operations, message customers, and (admins) generate invite codes
The system manages a fleet of 20 boats across 6 physical boat slips, with business logic that automatically handles slip assignment, overflow reservations, and end-of-day return enforcement.
| Layer | Technology |
|---|---|
| Mobile (iOS + Android) | React Native via Expo (SDK 54) |
| Routing | Expo Router (file-based) |
| Backend & Database | Supabase (Postgres) |
| Authentication | Supabase Auth |
| Real-time sync | Supabase Realtime (postgres_changes) |
| Scheduled jobs | pg_cron (via Supabase) |
| Push notifications | Expo Notifications + Supabase Edge Functions |
| Maps | @rnmapbox/maps (requires dev build) |
| Session storage | AsyncStorage |
marina-app/
├── app/
│ ├── (auth)/
│ │ ├── login.tsx # Shared login screen
│ │ ├── register.tsx # Customer self-registration
│ │ └── worker-register.tsx # Worker/admin invite-code registration
│ ├── (customer)/
│ │ ├── _layout.tsx
│ │ ├── index.tsx # Customer dashboard
│ │ ├── boats.tsx # My boats + add boat
│ │ ├── reservations.tsx # Make a reservation
│ │ ├── my-reservations.tsx # View + cancel reservations
│ │ ├── notifications.tsx # Inbox + system notifications
│ │ └── map.tsx # Boat location (stubbed in Expo Go)
│ ├── (worker)/
│ │ ├── _layout.tsx
│ │ ├── index.tsx # Worker dashboard
│ │ ├── boats.tsx # Boat approvals + archive/delete
│ │ ├── schedule.tsx # Slip board + daily schedule
│ │ ├── messages.tsx # Worker message inbox
│ │ ├── chat/[customerId].tsx # Threaded conversation with a customer
│ │ ├── invite-codes.tsx # Admin invite code management
│ │ └── map.tsx # Marina map (stubbed in Expo Go)
│ └── _layout.tsx # Root layout + auth gate
├── src/
│ ├── lib/
│ │ ├── supabase.ts # Supabase client
│ │ ├── boatService.ts # Boat CRUD + location pinning
│ │ ├── inviteCodeService.ts # Invite code generation
│ │ ├── notifications.ts # Push token registration
│ │ ├── notificationService.ts # Push notification sending
│ │ ├── mapbox.ts # Mapbox init (dev build only)
│ │ └── constants.ts # Marina coordinates + slip locations
│ ├── services/
│ │ ├── reservationService.ts # Reservation RPC calls
│ │ └── workerService.ts # Worker schedule + status updates (canonical file)
│ ├── context/
│ │ └── AuthContext.tsx # Session, profile, role state + periodic role re-check
│ ├── hooks/
│ │ └── useAuth.ts
│ └── types/
│ └── database.ts
└── supabase/
├── functions/
│ └── send-push-notification/ # Edge function for push delivery
│ └── index.ts
└── migrations/
├── 001_initial_schema.sql
└── 002_customer_reservation_rpc.sql
| Table | Description |
|---|---|
profiles |
Extends Supabase auth users with full name, phone, role, and push token |
invite_codes |
One-time codes for worker/admin registration, with grants_role |
boats |
Customer boats with approval status and archive flag |
slips |
The 6 physical boat slips with availability status |
reservations |
All bookings with slip assignment, return alert flag, and worker notes |
boat_locations |
Worker-pinned GPS coordinates for boat tracking |
notifications |
Push notification queue and history |
messages |
Worker to customer direct messages (threaded) |
user_role: customer | worker | admin
boat_status: in_water | on_land | maintenance
reservation_status: pending | confirmed | in_water | returned | overdue | cancelled
notification_type: reservation_confirmed | return_reminder | return_overdue |
slip_assigned | slip_released | worker_message | location_updatedThe marina has 6 physical slips reserved exclusively for boats staying past 5:00 PM. Early returners do not need a slip — the forklift operator handles them when they return.
Customer books with return before 5:00 PM
-> No slip assigned
-> Forklift pulls boat on return
Customer books with return at or after 5:00 PM
-> System checks: how many slips are held by other late stayers?
-> If a slip is available -> assigned automatically
-> If all 6 slips are taken by late stayers -> overflow reservation
-> needs_return_alert = true
-> Customer must return by 4:45 PM
The RPC public.create_customer_reservation uses row-level locking (FOR UPDATE SKIP LOCKED) to prevent race conditions during concurrent bookings, and includes optimistic-concurrency checks so a status or slip change is rejected (with a clear message) if the underlying row changed since it was last loaded.
- Time slots are in 15-minute increments from 9:00 AM to 5:00 PM
- One boat per time slot (the forklift handles one boat at a time)
- A reservation blocks both its put-in time slot and its return time slot
- Taken slots are hidden from the time picker for other customers
- The RPC rejects bookings for already-taken slots as a server-side safeguard
A pg_cron job runs every 15 minutes and marks no-slip in_water reservations as overdue if their expected return has passed or it's past 4:45 PM. Slip boats are never marked overdue (they stay overnight by design).
A pg_cron job runs at exactly 4:45 PM daily and notifies all in_water no-slip customers to return immediately.
Self-service via the registration screen. Email confirmation disabled for development.
Invite-code gated. The invite code carries a grants_role so a code can create either a worker or an admin. Codes are redeemed atomically via a row-locked database function to prevent two people redeeming the same code at once, and single-use codes expire after 7 days.
Admins can generate, copy, and deactivate invite codes from inside the app (Invite Codes screen). To create the first admin, manually update a profile's role to admin in SQL.
The root layout reads the user's role after login and routes:
customer->/(customer)/workeroradmin->/(worker)/
A periodic re-check keeps an already-logged-in session in sync if a role changes elsewhere (e.g. an admin promotes or demotes someone while they're still signed in).
- Expo project with TypeScript and expo-router
- Full Supabase schema with RLS on all tables
- Role-based auth (customer self-register, worker/admin invite-code)
- Automatic profile creation via database trigger
- Role-based navigation gate
- Login, customer registration, and worker registration screens
- Customer boat registration with name, make/model, and boat number
- Worker boat approval and rejection flow with reason
- Reservation system with correct 6-slip business logic
- Calendar date picker with past-date blocking
- Customer reservations list (today + upcoming)
- Cancel reservation with confirmation (race-guarded against concurrent worker updates)
- Multi-boat support per customer
- Worker daily schedule with date navigation (all dates)
- Slip board with 6 slips (available / occupied / overdue / unavailable)
- Worker can mark slips unavailable and restore them
- No-slip boats section with 4:45 PM urgent alert
- Summary bar (slips used, no-slip boats, overdue)
- Reservation action sheet with full boat/customer details
- Mark boats in water / returned / overdue, with undo paths in both directions
- Expandable "Returned (N)" section so returned boats stay reachable for undo
- Slip reassignment
- Overdue auto-trigger via pg_cron
- Real-time sync between worker devices, including live refresh of an open action sheet
- Boat archive (worker) / restore / permanent delete (admin)
- Archived boats show as inactive to the customer
- Mapbox map screens written (worker pin-drop + customer view)
- Slip and yard location markers defined
- Requires a development build to run (stubbed with placeholder screens in Expo Go for now)
- Push token registration and saving
- Edge function delivering push notifications to device (working)
- Worker messages customer from action sheet, in a threaded conversation view
- Customer inbox with unread badge
- Customer can reply to worker messages
- Worker message inbox with unread badge and reply
- Customers can message staff once they have at least one approved boat
- Reservation confirmed notification
- Boat status update notifications (non-blocking)
- 4:45 PM deadline alert scheduled
- Keyboard-avoiding inputs on all message screens
- 15-minute time slots with forklift buffer blocking
- Admin invite code generation (auto + custom codes, worker/admin roles)
- Status flow undo paths (return -> undo, overdue -> back to in water)
- Archive warning when a boat has upcoming reservations
- Flag reservations for review when their boat is archived
- Non-blocking notifications so push failures never crash core actions
- Concurrency hardening: optimistic concurrency checks on status/slip changes, live action-sheet refresh via realtime, race guard on customer cancellations
- Account/auth hardening: periodic role re-check while logged in, atomic invite-code redemption, an 8-second timeout safeguard for an auth cold-start hang
- Boat & reservation integrity edge cases (customer with zero boats reaching the reservation screen, duplicate boat resubmission, reservation date/timezone edge cases)
- Messaging edge cases (a demoted/deleted worker's existing conversations, a customer's last approved boat archived before staff reply)
- Notification edge cases (a stale push token after reinstall or a new device)
| Phase | Feature |
|---|---|
| 6 | Remaining edge case sweeps (boat/reservation integrity, messaging, notifications — see above) |
| 6 | Development build to enable Mapbox |
| 6 | Test maps with real marina coordinates |
| 6 | Final QA |
| 6 | App Store and Play Store builds |
- Node.js 18+
- Expo CLI and EAS CLI (
npm install -g expo-cli eas-cli) - Supabase account
- Expo account (expo.dev)
- Docker Desktop (required for deploying edge functions)
- Expo Go app on a physical device
git clone https://github.com/bqhorsfall/marina-app.git
cd marina-app
npm install --legacy-peer-depsCreate a .env file in the project root (gitignored — never commit this file):
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key-here
MAPBOX_SECRET_TOKEN=your-mapbox-secret-token-here
Reference these via process.env inside app.config.ts rather than hardcoding any key or token as a literal string in source.
- Run the migrations in the Supabase SQL editor
- Seed the slips and an invite code:
insert into public.slips (slip_number) values (1),(2),(3),(4),(5),(6);
insert into public.invite_codes (code, grants_role, expires_at)
values ('MARINA-WORKER-2025', 'worker', now() + interval '365 days');- Schedule the cron jobs:
select cron.schedule('mark-overdue-reservations', '*/15 * * * *',
'select public.mark_overdue_reservations()');
select cron.schedule('send-445-alerts', '45 16 * * *',
'select public.send_445_alerts()');supabase login
supabase link --project-ref your-project-ref
supabase functions deploy send-push-notification --no-verify-jwtIf the function serves stale code after a CLI deploy, edit it directly in the Supabase dashboard editor and save there — this has been the reliable fallback in this project.
npx expo start --clearScan the QR code with Expo Go. Both devices must be on the same WiFi network.
- Map-based location tracking requires a development build (not available in Expo Go).
- Email confirmation is disabled in development — enable before production.
- Apple App Store distribution requires a $99/year Apple Developer account.
- Never commit
.envto version control. - Row-level security is enabled on all tables.
- Invite codes are redeemed atomically (row-locked) and expire after 7 days.
- Admin-only actions check role via the profiles table in RLS policies rather than a self-referencing policy on
profilesitself, which avoids infinite recursion. - The service role key lives only in the edge function — never expose it to the client.
- Never hardcode a secret token (Mapbox secret token, service role key, etc.) directly in a source file such as
app.config.ts— reference it via an environment variable instead. - If a secret is ever accidentally committed, rotate it immediately in the provider's dashboard and remove it from git history (e.g. with
git filter-repo) before pushing again — deleting it from the current file alone is not sufficient, since GitHub's push protection scans full commit history.
Built with React Native, Expo, and Supabase.