Shippy is a web-based command center that connects ship owners, port managers, administrators, and public stakeholders around shared maritime data. The platform delivers authenticated dashboards, moderation workflows, and real-time ETA visibility while keeping a public lookup available for casual visitors.
- Ship owners register and maintain detailed vessel records (type, IMO number, capacities, current position).
- Port managers create and update port directories with geographic coordinates and operational status.
- Administrators moderate both domains, approve submissions, and enforce data quality.
- All authenticated personas query the ETA service while the public can run read-only lookups.
- Every mutating action generates audit trails and notification payloads.
- Install Expo Go on your iOS or Android device.
- Open Expo Go and scan the QR code above to launch (using your camera for IOS) the published Shippy mobile app demo.
- 🔐 JWT authentication & RBAC for Admin, Ship Owner, Port Manager, and Viewer personas using a shared React context.
- 🧭 Self-service registration so ship owners, port managers, and viewers can onboard themselves and receive a JWT instantly.
- 🚢 Ship management UI + API with creation, editing, status transitions, and admin approval controls.
- ⚓ Port management UI + API mirroring the ship experience with geographic coordinate capture.
- ⏱️ ETA integration that calls the external adapter, caches results, and exposes both private and public lookups with live arrival status reporting.
- 🛡️ Admin moderation hub showing pending records with approve/delete actions and an audit history view.
- ✉️ Notification hooks that log outgoing email events (confirmation, approval, admin alerts) via the demo email client.
npm install --prefix apps/server
npm install --prefix apps/web
npm install --prefix apps/mobilenpm run dev --prefix apps/serverThe API listens on http://localhost:4000.
npm run dev --prefix apps/webThe web app runs at http://localhost:5173.
npm run start --prefix apps/mobileExpo Dev Tools open in your browser; scan the QR code with Expo Go or press i/a to launch the iOS and Android simulators. Create apps/mobile/.env to override defaults:
EXPO_PUBLIC_API_BASE_URL=http://localhost:4000/api
EXPO_PUBLIC_SUPPORT_EMAIL=support@shippy.testUse your LAN IP in EXPO_PUBLIC_API_BASE_URL when testing on a physical device (http://<LAN-IP>:4000/api) and make sure the API is running first.
- Install the EAS CLI if needed:
npm install -g eas-cli(or run the commands withnpx eas update ...). - Authenticate once with
eas login. - Publish an update:
eas update --branch production --message "initial publish". - Share the link printed by the CLI or copy it from
https://expo.dev/accounts/<account>/projects/<slug>.
Environment variables are documented in .env.example. The client relies on VITE_API_BASE_URL; the API expects PORT, ALLOWED_ORIGINS, JWT_SECRET, ETA_API_BASE_URL, ETA_API_TIMEOUT (in milliseconds), ETA_API_ENDPOINT_PATH (defaults to /getETA), and the ship location sync variables detailed below.
- For local npm workflows, change
VITE_API_BASE_URLtohttp://localhost:4000/apiand access the React dev server athttp://localhost:5173. - When building for Docker Compose, leave the default
VITE_API_BASE_URL=http://localhost:9002/apiso the browser hits the host-mapped API port while the Express app continues to listen on4000inside the container. ALLOWED_ORIGINSalready whitelists bothhttp://localhost:5173(npm) andhttp://localhost:9003(Docker Compose).- The Expo mobile client reads
EXPO_PUBLIC_API_BASE_URL; point it athttp://<LAN-IP>:4000/apiwhen running on a physical device so the app reaches your dev server.
The repository includes a production-oriented Docker setup for both the API and the client. Copy the sample environment file so Docker Compose can load the configuration for each service:
cp .env.example .envBefore building, confirm .env retains the Docker default (VITE_API_BASE_URL=http://localhost:9002/api). The compose file exposes host ports 9002 and 9003 while the API itself keeps listening on 4000; the client build only needs the host-facing API URL to line up with that mapping. Switch back to http://localhost:4000/api only when you return to npm-based development.
Build and launch the stack:
docker compose up --build- The API will be available at
http://localhost:9002on the host while continuing to run on port4000inside the container. - The React client is published at
http://localhost:9003, leaving the Vite dev default (5173) untouched for npm-based workflows. - Update
.envwhenever you need to change ports or downstream integrations; both containers reuse the same file.
Stop the stack with docker compose down.
A background worker refreshes ship locations every five minutes by calling the configured location provider. The job starts automatically when the API boots and can be disabled or tuned via environment variables.
| Variable | Description | Example |
|---|---|---|
SHIP_LOCATION_SYNC_ENABLED |
Enables (true) or disables (false) the background updater. Defaults to true. |
false |
SHIP_LOCATION_SYNC_INTERVAL_MS |
Override for the polling cadence in milliseconds. Defaults to 300000 (five minutes). |
120000 |
SHIP_LOCATION_API_BASE_URL |
Base URL for the upstream provider serving live positions. | https://location.example.com |
SHIP_LOCATION_API_ENDPOINT_PATH |
Path appended to the base URL for the request. Defaults to /location. |
/v2/location |
SHIP_LOCATION_API_TIMEOUT |
Timeout in milliseconds applied to outbound HTTP calls. Defaults to 5000. |
8000 |
The external API must accept a GET request with the query parameter imoNumber=<IMO_number> and return either a currentPosition string or latitude/longitude values. Responses lacking usable coordinates are ignored with a warning log.
The ETA module calls an external HTTP API to resolve the expected arrival time for a ship/port pair. The integration is fully driven by environment variables so that different providers can be plugged in without code changes.
| Variable | Description | Example |
|---|---|---|
ETA_API_BASE_URL |
Base URL of the upstream ETA provider. | https://eta.example.com |
ETA_API_ENDPOINT_PATH |
Path appended to the base URL when performing lookups. Defaults to /getETA. |
/getETA |
ETA_API_TIMEOUT |
Request timeout in milliseconds applied to the outbound call. Defaults to 5000. |
8000 |
- HTTP method:
GET - URL:
${ETA_API_BASE_URL}${ETA_API_ENDPOINT_PATH} - Query parameters:
shipId– UUID for the ship requesting an ETA.portId– UUID for the destination port.
Example request:
GET https://eta.example.com/getETA?shipId=37d1d0a5-1dbf-4b58-9d1a-5a2d4b92eb0c&portId=1d639a1f-86b1-4d72-8fa5-536f8ae12b2a
The provider should respond with a JSON payload containing the calculated ETA, the most recent actual arrival (when available), and an arrival status of delayed, on_time, or early so clients can present accurate messaging. Any additional metadata is ignored by Shippy, but it can be present.
{
"shipId": "37d1d0a5-1dbf-4b58-9d1a-5a2d4b92eb0c",
"portId": "1d639a1f-86b1-4d72-8fa5-536f8ae12b2a",
"scheduledArrival": "2024-03-25T12:00:00.000Z",
"expectedArrival": "2024-03-25T14:30:00.000Z",
"actualArrival": "2024-03-25T13:58:00.000Z",
"arrivalStatus": "early",
"source": "provider-name"
}If the upstream service is unavailable, Shippy will fall back to a simulated ETA three hours in the future and cache the response for 15 minutes. The final API response delivered to clients always includes shipId, portId, scheduledArrival (when supplied by the provider), expectedArrival, actualArrival (when known), arrivalStatus, and lastUpdated.
| Role | Password | |
|---|---|---|
| Admin | admin@shippy.test |
Password123! |
| Ship Owner | owner@shippy.test |
Password123! |
| Port Manager | manager@shippy.test |
Password123! |
| Viewer | viewer@shippy.test |
Password123! |
Viewer accounts have read-only access and are redirected to the public ETA workspace.
| Field | Type | Notes |
|---|---|---|
| id | UUID | Primary key |
| name | string | Required |
| type | string | Required |
| IMO_number | string | Unique |
| DWT | number | Deadweight tonnage |
| capacity | number | Cargo capacity |
| ownerId | UUID | References User.id |
| currentPosition | string | Lat/long or port code |
| status | enum(pending,approved,inactive,under_maintenance) |
Workflow state |
| createdAt / updatedAt | ISO string | Audit timestamps |
| Field | Type | Notes |
|---|---|---|
| id | string | External port identifier |
| name | string | Required |
| country | string | Required |
| coordinates | object | { latitude: number, longitude: number } |
| capacity | number? | Optional throughput |
| managerId | UUID | References User.id |
| status | enum(pending,operational,closed,under_maintenance) |
Operational posture |
| createdAt / updatedAt | ISO string | Audit timestamps |
| Field | Type | Notes |
|---|---|---|
| shipId | UUID | References Ship.id |
| portId | UUID | References Port.id |
| scheduledArrival | ISO string | null | Timetabled arrival when provided by upstream |
| expectedArrival | ISO string | Calculated ETA |
| actualArrival | ISO string | null | Actual arrival reported by provider |
| lastUpdated | ISO string | Cache timestamp |
| Field | Type | Notes |
|---|---|---|
| id | UUID | Primary key |
| entityType | string | ship or port |
| entityId | UUID | Target entity |
| action | string | create, update, approve, delete |
| actorId | UUID | References User.id |
| timestamp | ISO string | Event time |
| changes | object? | Diff payload |
All authenticated endpoints require Authorization: Bearer <token>.
POST /api/auth/loginPOST /api/auth/registerGET /api/auth/roles
GET /api/ships(supports?status=)GET /api/ships/:idPOST /api/shipsPUT /api/ships/:idPATCH /api/ships/:id/approveDELETE /api/ships/:id
GET /api/ports(supports?status=)GET /api/ports/:idPOST /api/portsPUT /api/ports/:idPATCH /api/ports/:id/approveDELETE /api/ports/:id
GET /api/eta?shipId={id}&portId={id}– authenticated lookupGET /api/public/eta?shipId={id}&portId={id}– public lookup
GET /api/public/ship/:idGET /api/public/port/:id
GET /api/audit
GET /health
| Route | Access | Description |
|---|---|---|
/ |
Auth & public | Landing for guests, metrics dashboard for authenticated users |
/login |
Public | Email/password login with demo shortcuts |
/ships |
Ship Owner, Admin | Ship management workspace |
/ports |
Port Manager, Admin | Port management workspace |
/eta |
Ship Owner, Port Manager, Admin | Authenticated ETA lookup |
/admin |
Admin | Moderation queues |
/audit |
Admin | Audit trail viewer |
/public/eta |
Public | Read-only ETA search |
- Ship submission confirmation (queued email notification).
- Port submission confirmation (queued email notification).
- Admin alert for new pending records (queued notification).
- Metrics tracked in-memory: number of ships/ports, pending counts, ETA cache hits.
- React 18 + Vite + Material UI.
- React Router DOM with a custom
RequireAuthguard. - Native
fetchhelper for REST integration. - Auth state persisted via a custom context provider.
- Node.js + Express with modular routers per domain.
- JWT authentication, bcrypt password hashing, and RBAC middleware.
- In-memory seed store (Prisma + PostgreSQL planned for persistence).
- Winston + Morgan structured logging and rate limiting via
express-rate-limit. - Axios-based ETA integration client with graceful degradation.
- Vitest + Supertest integration tests.
- Shared type documentation in
packages/typesfor ships, ports, audit entries, and ETA payloads. - Centralized audit log service feeding the
/api/auditendpoint. - Demo email client capturing notifications for testing.
- Swap the in-memory store for Prisma/PostgreSQL and wire Swagger documentation.
- Extend email notifications to a real provider and add user-managed subscriptions.
- Introduce analytics dashboards (ETA success rates, lookup trends).
- Add React Testing Library coverage for core UI workflows.
