36 KiB
Perauset Frontend Architecture
Single Next.js application with Mantine v8, server-side rendering, and Pikku RPC for data fetching. Role-based UI adapts the experience from a focused guest view to a full operations dashboard for coordinators and admins.
Table of Contents
- Roles and Permissions Summary
- Navigation Structure
- Route Map
- Page Specifications
- Shared Components
- Data Fetching Strategy
- Mobile Considerations
- Authentication Flow
1. Roles and Permissions Summary
The backend defines 8 roles. The frontend groups them into three tiers for UI purposes:
| Tier | Roles | What they see |
|---|---|---|
| Admin/Coordinator | admin, coordinator |
Full operations dashboard: all domains, review queues, audit log, user management |
| Operational | facilitator, boat_coordinator, kitchen_lead, staff |
Domain-specific management views plus personal tools. Each role sees expanded UI for their domain. |
| Community | volunteer, community_member, guest |
Personal dashboard: own stay, own requests, claimable tasks, meal schedule, dietary profile |
The frontend does NOT enforce permissions -- the backend does via tags. The frontend uses the session's roles array to determine which navigation items, pages, and action buttons to render. If a user lacks the permission for a page, the server call will fail with 403, and the UI shows a "not authorized" state.
2. Navigation Structure
Layout: Responsive AppShell
- Desktop (>= 768px): Collapsible sidebar (240px open, 60px collapsed) + top bar with notifications and user menu
- Mobile (< 768px): Bottom tab bar (5 primary items) + hamburger drawer for secondary navigation
Sidebar Sections (Desktop)
[Logo / App Name]
-- PERSONAL --
Dashboard (all authenticated users)
My Stay (all authenticated users)
My Requests (all authenticated users)
Notifications (all authenticated users)
-- OPERATIONS -- (staff+ roles only)
Stays & Rooms (stays.view_all OR rooms.view_all)
Boat Schedule (boats.view_all OR boats.manage)
Task Board (tasks.view_all)
Retreats (retreats.view OR retreats.manage)
Kitchen (kitchen.view OR kitchen.manage)
Inventory (inventory.manage OR inventory.request)
-- ADMIN -- (admin, coordinator only)
Users (users.view)
Audit Log (audit_log.view)
Settings (admin only)
[User Avatar / Menu]
Profile & Dietary
Sign Out
Mobile Bottom Tab Bar
| Tab | Icon | Route | Visible to |
|---|---|---|---|
| Home | house | / |
All |
| Boats | ship | /boats |
All (request); operational roles see more |
| Tasks | check-square | /tasks |
All (claimable); staff+ see board |
| Meals | utensils | /kitchen |
All |
| More | menu | drawer | All (opens sidebar content) |
3. Route Map
All routes are under the Next.js App Router. Auth-gated routes use a middleware check; role-gated routes render a <RoleGate> wrapper that redirects or shows an empty state.
Public Routes
| Route | Purpose |
|---|---|
/login |
Credentials login form (Auth.js) |
/auth/error |
Auth error display |
Authenticated Routes -- Personal
| Route | Page Title | Min. Role | API Calls |
|---|---|---|---|
/ |
Dashboard | any | listStays(userId=me), listNotifications(limit=5), listTasks(status=open, limit=5), listBoatTrips(from=today, limit=3) |
/stay |
My Stay | any | listStays(userId=me, status=checked_in), listStayRequests(userId=me) |
/stay/request |
Request a Stay | stays.request |
requestStay (mutation), listRetreats(status=published) |
/requests |
My Requests | any | listStayRequests(userId=me), listBoatRequests(userId=me), listInventoryRequests(userId=me) (Note: some of these filters need to be added to the backend or the frontend filters client-side) |
/notifications |
Notifications | any | listNotifications, markNotificationRead |
/profile |
Profile & Dietary | any | getUser(userId=me), upsertDietaryProfile |
Authenticated Routes -- Boats
| Route | Page Title | Min. Permission | API Calls |
|---|---|---|---|
/boats |
Boat Schedule | boats.request |
listBoatTrips(from=today) |
/boats/trips/:tripId |
Trip Detail | boats.request |
listBoatTrips, listBoatRequests(tripId) |
/boats/trips/:tripId/request |
Request Seat | boats.request |
requestBoatSeat (mutation) |
/boats/manage |
Boat Management | boats.manage |
listBoatTrips, listBoatRequests |
/boats/manage/trips/new |
Create Trip | boats.manage |
createBoatTrip (mutation) |
/boats/manage/routes |
Route Management | boats.manage |
createBoatRoute (mutation) |
/boats/manage/fleet |
Fleet (Boats) | boats.manage |
createBoat (mutation) |
Authenticated Routes -- Tasks
| Route | Page Title | Min. Permission | API Calls |
|---|---|---|---|
/tasks |
Task Board | tasks.claim |
listTasks |
/tasks/:taskId |
Task Detail | tasks.claim |
listTasks, claimTask, startTask, completeTask, markTaskBlocked |
/tasks/manage |
Task Management | tasks.manage |
listTasks, createTaskInstance, assignTask, updateTask |
/tasks/manage/templates |
Task Templates | tasks.manage |
createTaskTemplate |
Authenticated Routes -- Retreats
| Route | Page Title | Min. Permission | API Calls |
|---|---|---|---|
/retreats |
Retreats List | retreats.view |
listRetreats |
/retreats/:retreatId |
Retreat Detail | retreats.view |
listRetreats(id), schedule items, persons |
/retreats/manage |
Retreat Management | retreats.manage |
listRetreats, createRetreat, updateRetreat |
/retreats/manage/:retreatId |
Edit Retreat | retreats.manage |
updateRetreat, addRetreatPerson, addRetreatScheduleItem, publishRetreat, cancelRetreat |
Authenticated Routes -- Kitchen
| Route | Page Title | Min. Permission | API Calls |
|---|---|---|---|
/kitchen |
Meal Schedule | kitchen.view |
listMealServices(from=today) |
/kitchen/dietary |
My Dietary Profile | kitchen.update_dietary |
upsertDietaryProfile |
/kitchen/manage |
Kitchen Board | kitchen.manage |
listMealServices, createMealService |
Authenticated Routes -- Stays & Rooms (Operational)
| Route | Page Title | Min. Permission | API Calls |
|---|---|---|---|
/stays |
All Stays | stays.view_all |
listStays, listStayRequests |
/stays/requests |
Stay Request Queue | stays.review |
listStayRequests(status=submitted), approveStayRequest, rejectStayRequest |
/stays/:stayId |
Stay Detail | stays.view_all |
listStays(stayId), checkInStay, checkOutStay, cancelStay |
/rooms |
Room Overview | rooms.view_all |
listRooms, listRoomAllocations |
/rooms/manage |
Room Management | rooms.manage |
createRoom, updateRoom, allocateRoom, releaseRoom |
Authenticated Routes -- Inventory
| Route | Page Title | Min. Permission | API Calls |
|---|---|---|---|
/inventory |
Inventory List | inventory.manage |
listInventory |
/inventory/request |
Request Supplies | inventory.request |
requestInventory (mutation) |
/inventory/requests |
Request Queue | inventory.manage |
list requests, reviewInventoryRequest, fulfillInventoryRequest |
Authenticated Routes -- Admin
| Route | Page Title | Min. Permission | API Calls |
|---|---|---|---|
/admin/users |
User Directory | users.view |
listUsers |
/admin/users/:userId |
User Detail | users.view |
getUser, updateUserProfile, setUserRoles |
/admin/audit |
Audit Log | audit_log.view |
listAuditLog |
4. Page Specifications
4.1 Dashboard (/)
The dashboard is role-adaptive. Every user sees it, but the content blocks differ.
Layout: Single-column stack on mobile, 2-column grid on desktop.
Blocks (conditional on role):
| Block | Component | Visible to | Data |
|---|---|---|---|
| Welcome banner | WelcomeBanner |
All | Session user name, current date |
| Active stay card | ActiveStayCard |
All | listStays(userId=me, status=checked_in) -- shows room, dates, check-out date |
| Upcoming boats | UpcomingBoatsList |
All | listBoatTrips(from=today, limit=3) -- next departures |
| My tasks | MyTasksList |
tasks.claim |
listTasks(assignedTo=me, status!=completed) |
| Claimable tasks | ClaimableTasksList |
tasks.claim |
listTasks(status=open, limit=5) |
| Unread notifications | NotificationPreview |
All | listNotifications(limit=5, unreadOnly=true) |
| Stay requests pending | PendingStayRequestsBadge |
stays.review |
listStayRequests(status=submitted) -- count |
| Boat requests pending | PendingBoatRequestsBadge |
boats.manage |
listBoatRequests(status=submitted) -- count |
| Inventory requests pending | PendingInventoryBadge |
inventory.manage |
count of submitted inventory requests |
| Today's meals | TodayMealsSummary |
kitchen.view |
listMealServices(date=today) |
| Occupancy summary | OccupancySummary |
rooms.view_all |
listStays(status=checked_in) count vs. listRooms total capacity |
| Retreat in progress | ActiveRetreatCard |
retreats.view |
listRetreats(status=active) |
Key interactions:
- Each card links to its detail page
- Notification items can be marked read inline
- Pending-request badges are clickable shortcuts to the review queue
4.2 My Stay (/stay)
Purpose: Guest/community view of their current or upcoming stay.
Components:
| Component | Description |
|---|---|
StayStatusTimeline |
Visual timeline: Requested > Approved > Checked In > Checked Out. Highlights current step. |
StayDetailCard |
Dates, room allocation (if any), stay type, associated retreat |
RequestStayButton |
Mantine Button linking to /stay/request. Shown when no active stay exists. |
StayRequestList |
Table of the user's past and pending stay requests with status badges |
Data flow:
- SSR:
listStays(userId=session.userId)+listStayRequests(userId=session.userId)(Note: backend may need a "my requests" variant or the frontend filters by userId client-side after fetching)
4.3 Request a Stay (/stay/request)
Purpose: Form to submit a stay request.
Components:
| Component | Description |
|---|---|
StayRequestForm |
Mantine form: requestType (select), date range picker (requestedStartAt, requestedEndAt), optional retreat selector (populated from listRetreats(status=published)), notes textarea |
RequestConfirmation |
Success state after submission showing request ID and "submitted" status |
Data flow:
- SSR:
listRetreats(status=published)for the retreat dropdown - Client mutation:
requestStay
4.4 Stay Request Queue (/stays/requests)
Purpose: Coordinators review incoming stay requests.
Components:
| Component | Description |
|---|---|
StayRequestTable |
Mantine DataTable with columns: requester name, type, dates, retreat, status, submitted date. Filterable by status. |
StayRequestReviewDrawer |
Slide-out drawer on row click. Shows full details, user profile link. Approve/Reject buttons with optional review notes. |
StayRequestStatusFilter |
Segmented control: All / Submitted / Approved / Rejected |
Data flow:
- SSR:
listStayRequests(status=submitted) - Client mutations:
approveStayRequest,rejectStayRequest - After approval: prompt to
createStayFromRequest
4.5 All Stays (/stays)
Purpose: Operational view of all active and historical stays.
Components:
| Component | Description |
|---|---|
StayTable |
Paginated table: user, dates, type, status, room, check-in/out times |
StayFilterBar |
Status filter (segmented), date range picker, user search |
StayActions |
Per-row action menu: Check In, Check Out, Cancel, View Detail |
StayDetailDrawer |
Drawer with full stay info + room allocation + linked request |
Data flow:
- SSR:
listStays(limit=50),listRoomAllocations - Client mutations:
checkInStay,checkOutStay,cancelStay
4.6 Room Overview (/rooms)
Purpose: Visual display of all rooms and their current allocation status.
Components:
| Component | Description |
|---|---|
RoomGrid |
Card grid showing each room: name, code, type, capacity, current occupant(s). Color-coded by status (vacant=green, occupied=amber, blocked=red, maintenance=gray). |
RoomAllocationTimeline |
Optional Gantt-style timeline view of room allocations over a date range. Uses Mantine Timeline or a custom canvas component. |
RoomFilterBar |
Filter by type (dorm, private, shared, etc.), status |
RoomDetailDrawer |
Drawer showing room details, allocation history, block history |
Data flow:
- SSR:
listRooms,listRoomAllocations(from=today)
4.7 Room Management (/rooms/manage)
Purpose: Create rooms, allocate guests to rooms, manage blocks.
Components:
| Component | Description |
|---|---|
CreateRoomForm |
Modal form: name, code, type, capacity, notes |
EditRoomForm |
Same fields, pre-populated |
AllocateRoomForm |
Modal: select room, select stay, date range, reason |
ReleaseRoomButton |
Action on an active allocation |
RoomBlockForm |
Create a room block (maintenance, admin hold, retreat reserved) |
Data flow:
- Client mutations:
createRoom,updateRoom,allocateRoom,releaseRoom
4.8 Boat Schedule (/boats)
Purpose: View upcoming boat trips and request seats.
Components:
| Component | Description |
|---|---|
BoatScheduleList |
Card list (mobile) or table (desktop) of upcoming trips. Each shows: route (origin > destination), departure time, boat name, available seats indicator, status badge. |
TripCard |
Expanded view on click: full details + "Request Seat" button if status is open |
BoatScheduleCalendar |
Optional calendar view for desktop showing trips by day |
DateNavigator |
Day/week selector to browse the schedule |
Data flow:
- SSR:
listBoatTrips(from=today, status=open)
4.9 Trip Detail (/boats/trips/:tripId)
Components:
| Component | Description |
|---|---|
TripHeader |
Route, departure/arrival times, boat, status |
ManifestList |
List of confirmed passengers (visible to boats.view_all) |
RequestSeatButton |
Opens inline form or navigates to /boats/trips/:tripId/request |
RequestsList |
(boats.manage) Table of all seat requests for this trip with confirm/decline actions |
Data flow:
- SSR: trip detail from
listBoatTrips,listBoatRequests(tripId) - Client mutations:
requestBoatSeat,confirmBoatRequest,declineBoatRequest
4.10 Boat Management (/boats/manage)
Purpose: Full boat operations for boat coordinators.
Components:
| Component | Description |
|---|---|
TripManagementTable |
All trips (not just open). Status filter. Actions: open, update, cancel, complete. |
CreateTripForm |
Modal: select route, select boat, departure datetime, arrival datetime, notes |
RequestReviewPanel |
Pending seat requests across all trips. Bulk confirm/decline. |
RouteManagementTab |
CRUD for boat routes |
FleetManagementTab |
CRUD for boats (name, capacity, cargo capacity) |
Tabs within page: Trips | Requests | Routes | Fleet
Data flow:
- SSR:
listBoatTrips,listBoatRequests - Client mutations:
createBoatTrip,openBoatTrip,updateBoatTrip,cancelBoatTrip,completeBoatTrip,confirmBoatRequest,declineBoatRequest,createBoatRoute,createBoat
4.11 Task Board (/tasks)
Purpose: Kanban-style board of tasks. Community users see claimable tasks. Staff+ sees all tasks.
Components:
| Component | Description |
|---|---|
TaskKanban |
Columns: Open, Assigned, In Progress, Blocked, Completed. Each card shows title, category badge, location, scheduled time. Drag-and-drop on desktop (optional). |
TaskListView |
Alternative list view (default on mobile). Sortable by category, status, date. |
TaskCard |
Title, category pill, time, location. Click opens detail drawer. |
TaskDetailDrawer |
Full task info. Actions based on status: Claim (if open), Start (if assigned to me), Complete, Mark Blocked. |
TaskCategoryFilter |
Filter chips: housekeeping, kitchen, maintenance, operations, retreat, transport |
ViewToggle |
Board view / List view toggle |
Data flow:
- SSR:
listTasks - Client mutations:
claimTask,startTask,completeTask,markTaskBlocked
4.12 Task Management (/tasks/manage)
Purpose: Coordinators create tasks, assign to people, manage templates.
Components:
| Component | Description |
|---|---|
CreateTaskForm |
Modal: title, description, category, location, scheduled start/end, optional template, linked retreat/stay |
AssignTaskDrawer |
Select a user, add notes. Triggers assignTask. |
TaskTemplateTable |
List of templates with title, category, recurrence, claimable flag. Create/edit actions. |
CreateTemplateForm |
Modal: title, description, category, estimated minutes, default location, recurrence cron, isClaimable toggle |
Data flow:
- Client mutations:
createTaskInstance,assignTask,updateTask,createTaskTemplate
4.13 Retreats List (/retreats)
Purpose: Browse retreats. Community users see published/active retreats. Facilitators+ see all statuses.
Components:
| Component | Description |
|---|---|
RetreatCardGrid |
Card per retreat: name, dates, status badge, capacity indicator, description excerpt |
RetreatStatusFilter |
Tabs or segmented: All / Published / Active / Completed / Draft (draft only for retreats.manage) |
Data flow:
- SSR:
listRetreats
4.14 Retreat Detail (/retreats/:retreatId)
Components:
| Component | Description |
|---|---|
RetreatHeader |
Name, dates, status, capacity bar |
RetreatSchedule |
Timeline/agenda view of schedule items (title, time, location, lead person) |
RetreatParticipantList |
Table of persons: name, role (organizer/facilitator/participant), attendance status |
RetreatDescription |
Full description text |
Data flow:
- SSR: retreat detail, schedule items, persons (these may need dedicated list endpoints or be embedded in a "get retreat" response -- if not available, consider adding
getRetreatendpoint)
4.15 Retreat Management (/retreats/manage/:retreatId)
Purpose: Full CRUD for a retreat.
Components:
| Component | Description |
|---|---|
RetreatForm |
Edit name, description, dates, capacity, check-in/out defaults |
ScheduleEditor |
Add/edit/remove schedule items. Each item: title, start, end, location, lead person |
PersonManager |
Add persons (by user search or email/name for external). Set role type. Track attendance status. |
RetreatActions |
Publish, Cancel buttons with confirmation modals |
Data flow:
- Client mutations:
updateRetreat,addRetreatPerson,addRetreatScheduleItem,publishRetreat,cancelRetreat
4.16 Meal Schedule (/kitchen)
Purpose: View upcoming meals for all community members.
Components:
| Component | Description |
|---|---|
MealDayView |
Grouped by day, showing breakfast/lunch/dinner/snack cards. Each card: meal type icon, time, headcount, menu notes, status badge. |
MealDatePicker |
Navigate between days |
DietaryProfileLink |
Banner linking to /kitchen/dietary if profile is incomplete |
Data flow:
- SSR:
listMealServices(date=today)
4.17 Kitchen Board (/kitchen/manage)
Purpose: Kitchen lead manages meal services.
Components:
| Component | Description |
|---|---|
MealServiceTable |
Table with date, type, headcount, status, actions. Filterable by date range and meal type. |
CreateMealServiceForm |
Modal: date, type (breakfast/lunch/dinner/snack), planned headcount, menu notes, optional linked retreat |
MealStatusUpdater |
Quick status transitions: planned > ready > served > completed |
DietarySummary |
Aggregated dietary needs for a given date (count of vegetarian, vegan, gluten-free, etc. across expected attendees) |
Data flow:
- SSR:
listMealServices - Client mutations:
createMealService
4.18 Dietary Profile (/kitchen/dietary or /profile)
Components:
| Component | Description |
|---|---|
DietaryProfileForm |
Toggle switches: isVegetarian, isVegan, isGlutenFree, isDairyFree, hasNutAllergy. Text fields: allergyNotes, dislikes, otherRequirements. |
Data flow:
- Client mutation:
upsertDietaryProfile
4.19 Inventory (/inventory)
Components:
| Component | Description |
|---|---|
InventoryTable |
Table: name, unit, current qty, minimum qty, status indicator (low stock = qty < minimum). Sortable. |
LowStockAlert |
Banner at top if any items are below minimum |
RequestSuppliesButton |
Links to /inventory/request |
Data flow:
- SSR:
listInventory
4.20 Inventory Request (/inventory/request)
Components:
| Component | Description |
|---|---|
InventoryRequestForm |
Item selector (from inventory list or free-text name), quantity, unit, purpose |
Data flow:
- SSR:
listInventory(for item selector) - Client mutation:
requestInventory
4.21 Inventory Request Queue (/inventory/requests)
Components:
| Component | Description |
|---|---|
InventoryRequestTable |
Table: item name, qty, requester, status, date. Actions: review (approve/reject), fulfill. |
ReviewDrawer |
Review notes, approve or reject |
FulfillButton |
Marks a request as fulfilled |
Data flow:
- Client mutations:
reviewInventoryRequest,fulfillInventoryRequest
4.22 User Directory (/admin/users)
Components:
| Component | Description |
|---|---|
UserTable |
Paginated table: name, email, roles (pills), status, joined date |
UserSearchBar |
Search by name or email |
UserStatusFilter |
Active / Inactive / Archived |
Data flow:
- SSR:
listUsers
4.23 User Detail (/admin/users/:userId)
Components:
| Component | Description |
|---|---|
UserProfileCard |
Name, email, phone, status, date of birth, nationality, emergency contact |
RoleManager |
Multi-select of roles with save button. Uses setUserRoles. |
UserProfileEditForm |
Edit profile fields. Uses updateUserProfile. |
UserActivityTimeline |
Recent stays, tasks, requests (read-only, assembled from multiple API calls) |
Data flow:
- SSR:
getUser(userId) - Client mutations:
setUserRoles,updateUserProfile
4.24 Audit Log (/admin/audit)
Components:
| Component | Description |
|---|---|
AuditLogTable |
Paginated table: timestamp, user, action, table, record ID, changed fields (JSON expandable) |
AuditLogFilters |
Filter by table name, user, action, date range |
Data flow:
- SSR:
listAuditLog
4.25 Notifications (/notifications)
Components:
| Component | Description |
|---|---|
NotificationList |
Grouped by date. Each item: icon by type, title, body preview, timestamp, read/unread indicator. Click marks as read and navigates to linked entity. |
MarkAllReadButton |
Bulk action (may need backend endpoint or iterative calls) |
NotificationBell |
In top bar, shows unread count badge. Dropdown with last 5 notifications. |
Data flow:
- SSR:
listNotifications - Client mutation:
markNotificationRead
5. Shared Components
Layout Components
| Component | Description |
|---|---|
AppShell |
Mantine AppShell with sidebar, header, main content area. Responsive breakpoints. |
RoleGate |
Wrapper that checks session.roles against required permissions. Renders children or a "not authorized" message. |
SidebarNav |
Role-filtered navigation links. Active state via usePathname(). |
MobileTabBar |
Fixed bottom bar for mobile. 5 tabs with icons and labels. |
PageHeader |
Title, breadcrumbs, optional action buttons (top-right). |
NotificationBell |
Header icon with unread count, dropdown preview. |
Data Display Components
| Component | Description |
|---|---|
StatusBadge |
Mantine Badge with color mapped to status string. Reused across stays, boats, tasks, retreats, inventory. |
DateRangeDisplay |
Formatted date range (e.g., "Mar 15 - Mar 22, 2026") |
UserAvatar |
Avatar with name tooltip. Links to user detail for admin roles. |
PaginatedTable |
Wrapper around Mantine Table with limit/offset pagination controls. |
EmptyState |
Illustration + message + optional CTA button for empty lists. |
LoadingState |
Skeleton loaders matching the expected content shape. |
ConfirmModal |
Reusable confirmation dialog for destructive actions (cancel stay, reject request, etc.) |
Form Components
| Component | Description |
|---|---|
DateRangePicker |
Mantine DatePickerInput configured for range selection |
UserSelect |
Async searchable select that queries listUsers |
RoomSelect |
Select populated from listRooms |
RetreatSelect |
Select populated from listRetreats |
StatusFilter |
Segmented control or select for status enum values |
6. Data Fetching Strategy
Server-Side (SSR)
Next.js App Router server components call Pikku RPC functions directly (or via a server-side HTTP client to localhost:6002/api/...). This provides:
- Fast initial page loads
- SEO for any public pages (currently none, but future-proofed)
- Session cookie forwarded from the incoming request
// Example: app/stays/page.tsx (server component)
async function StaysPage() {
const stays = await pikkuClient.listStays({ limit: 50 })
return <StayTable data={stays} />
}
Client-Side (Mutations + Polling)
- All write operations (create, approve, reject, claim, etc.) are client-side via Pikku RPC
- After mutation success: invalidate relevant server data using
router.refresh()or React Query / SWR cache invalidation - Polling: notifications badge polls every 60 seconds via
listNotifications(limit=1, unreadOnly=true)to update the count - Optimistic updates for instant feedback on claim/complete task actions
Recommended Data Layer
Use @tanstack/react-query for client-side cache management:
- Query keys follow the pattern:
['domain', 'action', params](e.g.,['stays', 'list', { status: 'checked_in' }]) - Mutations invalidate related query keys on success
- Server components pass initial data as props; client components hydrate from those props
7. Mobile Considerations
The island context means many users will be on phones, often with intermittent connectivity.
Design Principles
- Touch-first: All interactive targets minimum 44x44px. Use Mantine's
size="lg"for buttons and inputs on mobile. - Single-column layouts: All pages stack to single column below 768px. No horizontal scrolling.
- Bottom-anchored actions: Primary actions (Request Seat, Claim Task, Check In) use sticky bottom action bars on mobile, not top-right buttons.
- Offline tolerance: Show last-fetched data with a "stale data" indicator when offline. Queue mutations and retry when connectivity returns (stretch goal -- implement in v2).
- Minimal data transfer: Use pagination aggressively. Default
limit=20on mobile (vs. 50 on desktop). Avoid loading images where text suffices.
Mobile-Specific Components
| Component | Description |
|---|---|
BottomActionBar |
Sticky bottom bar with primary action button(s). Replaces top-right action buttons on mobile. |
SwipeableCard |
Swipe-right to claim a task, swipe-left to dismiss. Provides fast interaction on task board. |
PullToRefresh |
Pull-down gesture triggers router.refresh() on list pages. |
CompactTable |
Table that collapses to a card-list layout on mobile, showing only key fields with expandable detail. |
8. Authentication Flow
Login (/login)
- Credentials form (email + password) using Auth.js
- On success: redirect to
/(dashboard) - Session stored as HTTP-only cookie
- Auth.js endpoints at
/auth/*(handled byauth.wiring.ts)
Session Management
- Middleware (
middleware.tsin Next.js root) checks for valid session on all routes except/loginand/auth/* - Session object shape:
{ userId: string, roles: string[] } - Roles array used by
RoleGateand sidebar filtering - Session refresh handled automatically by Auth.js JWT rotation
Logout
- POST to
/auth/signout(Auth.js endpoint) - Clear local state and redirect to
/login
Appendix: Complete API Endpoint Reference
For reference, here is every backend endpoint grouped by domain, with its HTTP method, route, and required permission tag.
System
| Method | Route | Permission | Function |
|---|---|---|---|
| GET | /health-check |
none (public) | healthCheck |
Users
| Method | Route | Permission | Function |
|---|---|---|---|
| GET | /users |
users.view |
listUsers |
| GET | /users/:userId |
users.view |
getUser |
| PUT | /users/:userId/profile |
users.manage |
updateUserProfile |
| GET | /roles |
authenticated | listRoles |
| PUT | /users/:userId/roles |
roles.assign |
setUserRoles |
Stays
| Method | Route | Permission | Function |
|---|---|---|---|
| POST | /stays/requests |
stays.request |
requestStay |
| POST | /stays/requests/:requestId/approve |
stays.review |
approveStayRequest |
| POST | /stays/requests/:requestId/reject |
stays.review |
rejectStayRequest |
| GET | /stays/requests |
stays.view_all |
listStayRequests |
| POST | /stays |
stays.manage |
createStayFromRequest |
| POST | /stays/:stayId/check-in |
stays.manage |
checkInStay |
| POST | /stays/:stayId/check-out |
stays.manage |
checkOutStay |
| POST | /stays/:stayId/cancel |
stays.manage |
cancelStay |
| GET | /stays |
stays.view_all |
listStays |
Rooms
| Method | Route | Permission | Function |
|---|---|---|---|
| POST | /rooms |
rooms.manage |
createRoom |
| PUT | /rooms/:roomId |
rooms.manage |
updateRoom |
| POST | /rooms/allocations |
rooms.manage |
allocateRoom |
| POST | /rooms/allocations/:allocationId/release |
rooms.manage |
releaseRoom |
| GET | /rooms |
rooms.view_all |
listRooms |
| GET | /rooms/allocations |
rooms.view_all |
listRoomAllocations |
Boats
| Method | Route | Permission | Function |
|---|---|---|---|
| POST | /boats/routes |
boats.manage |
createBoatRoute |
| POST | /boats |
boats.manage |
createBoat |
| POST | /boats/trips |
boats.manage |
createBoatTrip |
| POST | /boats/trips/:tripId/open |
boats.manage |
openBoatTrip |
| POST | /boats/trips/:tripId/request-seat |
boats.request |
requestBoatSeat |
| POST | /boats/requests/:requestId/confirm |
boats.manage |
confirmBoatRequest |
| POST | /boats/requests/:requestId/decline |
boats.manage |
declineBoatRequest |
| PUT | /boats/trips/:tripId |
boats.manage |
updateBoatTrip |
| POST | /boats/trips/:tripId/cancel |
boats.manage |
cancelBoatTrip |
| POST | /boats/trips/:tripId/complete |
boats.manage |
completeBoatTrip |
| GET | /boats/trips |
boats.view_all |
listBoatTrips |
| GET | /boats/requests |
boats.view_all |
listBoatRequests |
Tasks
| Method | Route | Permission | Function |
|---|---|---|---|
| POST | /tasks/templates |
tasks.manage |
createTaskTemplate |
| POST | /tasks |
tasks.manage |
createTaskInstance |
| POST | /tasks/:taskId/assign |
tasks.assign |
assignTask |
| POST | /tasks/:taskId/claim |
tasks.claim |
claimTask |
| POST | /tasks/:taskId/start |
tasks.manage |
startTask |
| PUT | /tasks/:taskId |
tasks.manage |
updateTask |
| POST | /tasks/:taskId/complete |
tasks.manage |
completeTask |
| POST | /tasks/:taskId/block |
tasks.manage |
markTaskBlocked |
| GET | /tasks |
tasks.view_all |
listTasks |
Retreats
| Method | Route | Permission | Function |
|---|---|---|---|
| POST | /retreats |
retreats.manage |
createRetreat |
| PUT | /retreats/:retreatId |
retreats.manage |
updateRetreat |
| POST | /retreats/:retreatId/persons |
retreats.manage |
addRetreatPerson |
| POST | /retreats/:retreatId/publish |
retreats.manage |
publishRetreat |
| POST | /retreats/:retreatId/cancel |
retreats.manage |
cancelRetreat |
| POST | /retreats/:retreatId/schedule |
retreats.manage |
addRetreatScheduleItem |
| GET | /retreats |
retreats.view |
listRetreats |
Kitchen
| Method | Route | Permission | Function |
|---|---|---|---|
| PUT | /kitchen/dietary-profile |
kitchen.update_dietary |
upsertDietaryProfile |
| POST | /kitchen/meal-services |
kitchen.manage |
createMealService |
| GET | /kitchen/meal-services |
kitchen.view |
listMealServices |
Inventory
| Method | Route | Permission | Function |
|---|---|---|---|
| POST | /inventory/requests |
inventory.request |
requestInventory |
| POST | /inventory/requests/:requestId/review |
inventory.manage |
reviewInventoryRequest |
| POST | /inventory/requests/:requestId/fulfill |
inventory.manage |
fulfillInventoryRequest |
| GET | /inventory |
inventory.manage |
listInventory |
Notifications
| Method | Route | Permission | Function |
|---|---|---|---|
| GET | /notifications |
authenticated | listNotifications |
| POST | /notifications/:notificationId/read |
authenticated | markNotificationRead |
Audit
| Method | Route | Permission | Function |
|---|---|---|---|
| GET | /audit-log |
audit_log.view |
listAuditLog |
Appendix: Backend Gaps Identified
During architecture design, several backend additions would improve the frontend experience. These are not blockers for v1 but should be prioritized for the first iteration.
| Gap | Impact | Suggested Endpoint |
|---|---|---|
| No "get single entity" endpoints for stay, retreat, trip, task | Detail pages must filter list endpoints or add dedicated getStay, getRetreat, getBoatTrip, getTask |
GET /stays/:stayId, GET /retreats/:retreatId, GET /boats/trips/:tripId, GET /tasks/:taskId |
| No "my requests" filter on stay requests | Guest cannot efficiently see only their own requests | Add userId filter param to listStayRequests |
| No "my boat requests" filter | Guest cannot see their own boat reservation requests | Add userId filter param to listBoatRequests |
| No "my inventory requests" view | Users who requested supplies can't track them | Add listInventoryRequests with userId filter |
| No "get retreat" with schedule + persons | Retreat detail page needs 3 separate calls or a compound endpoint | GET /retreats/:retreatId returning nested schedule + persons |
| No list endpoints for boat routes or boats (fleet) | Boat management UI needs to populate dropdowns | GET /boats/routes, GET /boats |
| No "mark all notifications read" | Common UX pattern, currently requires N calls | POST /notifications/mark-all-read |
| No unread notification count | Notification bell badge requires fetching full list | GET /notifications/unread-count |
listStayRequests filtered to own user for non-admin |
Guests with stays.request but not stays.view_all need to see their own requests |
Either a separate GET /stays/my-requests or backend auto-filters by session userId when stays.view_all is absent |