🔒
Restricted page
Roadmap is limited to superadmins and leadership. If you believe you should have access, ask a superadmin to check your role or team tags. Members tagged Tech can reach this page only while a superadmin has this section switched on (Team management → Tech access).
Back to OfficeDevelopment Roadmap
& Architecture
The build plan to take 5MLOS from a deploy boilerplate to a live presence-first virtual office — and the tech stack that powers it.
Development Roadmap
9 phases · presence-firstPhase 0
Foundation & Delivery
Done- CI/CD: GitHub Actions builds app + API images → Amazon ECR → Coolify pull-and-restart (
restart?latest=true) Shipped - Deploy consolidated into one workflow (single Coolify trigger + concurrency group) to stop the parallel-workflow race Shipped
- Edge routing via Coolify's Traefik proxy (Let's Encrypt, path split
/api/*+/ws→ API, rest → nginx; there is no Caddy on the host);main→ staging at 5ml-os.recruitai.studio Shipped - Production deploy path —
masterhas not moved since 2026-07-10 and is hundreds of commits behind, its workflows still use the cached-image webhook, and no Coolify resource is identified as production Planned — unresolved, seeserver/DEPLOY.md§3
GitHub ActionsDockerAWS ECRCoolifyTraefik
Phase 1
Identity & Live Presence
Done- Auth, orgs, floors & rooms data model Shipped
- Bird's-eye office map (2D Canvas) with avatars & status, home-seat vs. ephemeral room-visit, 8-desk cap + overflow, WhatsApp deep links Shipped
- Realtime presence over WebSockets (who's online / where / talking), with
last_seen_at"last online" tracking for the Team directory Shipped - Superadmin "Edit layout" mode on the 2D floor: drag-to-reassign home seats (server-persisted); room label/order/size/visibility overrides server-persisted too, via
GET/PUT /api/org/room-layout(routes/org.js, admin-gated, audited) Shipped
Vanilla JSWebSocket (ws)PostgresExpress
Phase 2
Drop-in Audio & Video
In progress- Room voice — WebRTC audio for whoever is in the same room, on the web (
roam-voice.js) and in the mobile app; STUN-only untilTURN_SECRET/TURN_URLSconfigure a relay (routes/voice.js,docs/voice-architecture.md) Shipped - Knock-to-join and click-to-talk affordances on top of room voice Planned
- Video conference rooms + screen share via an SFU Planned
WebRTCLiveKit / mediasoupTURN/STUN
Phase 3
Chat — AInbox
In progress- DMs, group chats (incl. auto-provisioned per-project team chat), one level of reply threading Shipped
- Custom folders & message bookmarking Planned — not yet built (message pinning —
POST /:id/messages/:mid/pin— has since shipped separately, see below) - Message search (case-insensitive, not yet full-text/tsvector) & desktop notifications for new messages Shipped
- 7-day default history window with "load older" pagination, image attachments (client-compressed, stored in Postgres, pencil/pin annotation), participant-only access on every conversation route Shipped
- Leave-a-group, per-participant archiving, conversation-list privacy scoping, unread badges (nav + rail + widget + per-conversation), and a self-healing live feed (reconnect + poll) Shipped
- Message edit/delete, emoji reactions, @mentions, message pinning, read receipts, typing indicators, and user-reported-message moderation (report → admin Reports queue) Shipped
- Link previews — SSRF-guarded server-side URL unfurl (fetch a shared link's title/description/image server-side, never client-side, with private-IP/redirect guarding) rendered as a preview card in chat (
server/src/routes/unfurl.js, consumed bychat.htmlandchat-widget.js) Shipped - File library for chats — a store for files that belong to a conversation, separate from images sent as messages (
223bc7d) Shipped (server side; the upload UI is the next pass)
WebSocketPostgres (BYTEA)Web Notifications API
Phase 4
AI Layer — Magic Minutes & Personal Assistant
In progress- Meeting transcription → AI summaries posted to chat Planned — no audio/STT pipeline yet
- Personal Assistant (shipped as "On-It", since renamed): a shared bot member with a private 1:1 chat per user, DeepSeek replies grounded in the user's own org context, notification mirroring and meeting reminders (
server/src/pa.js), plus a standalone popover-sized pagepa.html(e20c852); its system prompt was found to have been silently dropped since day one and is now actually sent (7f7f631) Shipped - Assistant actions — setting reminders, scheduling, creating action items on your behalf Planned
- Company Map — people, teams, projects, tasks and agents as one radial graph plus a people×projects matrix (
companymap.html,GET /api/companymap,b4f4ca9), with an adversarial leak-path pass closed before launch (a2ed094) Shipped - Workflow primitive —
workflows,workflow_steps,workflow_runs,workflow_run_tasksso a workflow is defined rather than hand-coded (8636e27); the four existing schedulers are not yet migrated onto it Shipped - Company vault —
docs/5ml-os-vault/, 53 notes with a status ladder and a link checker, blank where the repo has no source rather than plausible (a552841,e7e91cd) Shipped GET /api/status/config-sources— per-key provenance (file / env / unset) for the secrets that power AI and integrations, never the value (46f99e4) Shipped- Text-based AI helpers (DeepSeek-powered): office concierge, leave assistant, activity digest — all participant/role-gated Shipped (the chat-transcript "Summarize" button was pulled from the chat UI this session; the endpoint remains)
- AI usage/cost tracking (Track 0.1): per-user/org daily rate limits, a monthly spend ceiling using real DeepSeek peak/off-peak pricing, and a leadership/superadmin-only
ai-usage.htmlanalytics page Shipped (shipped alongside a same-day fix for a DeepSeek model-name outage —deepseek-chat/deepseek-reasonerwere retired 2026-07-24; default is nowdeepseek-v4-flash). Since extended with anai_usage.providercolumn, a "By backend" panel and a Backend column onai-usage.html, so a Hermes-spent token is attributable rather than merged into one total - The monthly spend ceiling had never actually enforced on Postgres — fixed.
costExprSql()built aCASEwith two untyped bind parameters, which Postgres resolved totext, so the multiply had no operator (operator does not exist: numeric * text):perUserTotals,orgTotalsandperProviderTotalsall threw,GET /api/ai-usage/summary500'd, andgetMonthlySpendUsd()caught the error and returned0— fail-open by design, which madeisOverMonthlySpendCeiling()permanently false. Measured before the fix: $2.64 of real month-to-date spend against a $0.50 ceiling, and the gate still returned ok. The bug predated the provider work and survived because no Postgres test touched a cost aggregation (the in-memory backend computes in JS and was unaffected, so the suite stayed green) Shipped - Hermes agent client + config-selected PM-agent backend (Tracks 0.3 & B):
server/src/hermes.js(fail-soft sibling to the DeepSeek client, no hardcoded default model), andSTATUS_SUGGESTION_BACKENDletting the project status-suggestion endpoint run through Hermes instead of DeepSeek (default unchanged, fails closed if misconfigured) Shipped — still inert: no live Hermes infrastructure exists, so neither path is actually reachable in production yet. What has changed is that a Hermes call is now metered and labelled if and when one runs: the label is derived from the same single read ofstatusSuggestionBackend()that picks the client, so it cannot drift from the backend actually used, and a call refused by the quota gate keeps its backend label too - Hermes-only knowledge/best-practices assistant (Track A):
POST /api/ai/knowledge-ask+ adashboard.htmlcard; sends zero Roam-internal data (unlike the org-data-grounded concierge wired intooffice3d.html), no DeepSeek fallback Shipped — also still inert pending real Hermes infrastructure; metered and provider-labelled on the same basis as Track B above - Hermes host provisioning (Track 0.2) In progress — both things this was blocked on have since resolved — and a third, expected blocker turned out not to exist (
docs/hermes-host-readiness.md). DNS is done:5ml-hermes.recruitai.studioresolves and already terminates on the host's proxy (it returns that proxy's own 404 and its default self-signed cert — i.e. the name is correct, there is simply no router for it yet). The host question is decided: co-locate on the existing shared Coolify box, weaker isolation accepted knowingly, rather than provision a separate host. And the expected edge blocker was not real:server/DEPLOY.md's hand-edited-Caddyfile instructions are stale for this host — the proxy is Traefik managed by Coolify, so settingdomainson the app is enough; no SSH, no Caddy reload. What is still outstanding is human-held, not technical: aDEEPSEEK_API_KEYfor Hermes itself and a generatedAPI_SERVER_KEY(the only thing between the public internet and Hermes's shell execution —API_SERVER_ENABLED/API_SERVER_HOSTmust both be overridden, and binding publicly without that key would expose it), a real disk/RAM reading off the host as a stated go/no-go gate, and confirmation that the Coolify API token can write at all. Attaching the domain is the point of no return: it publishes a shell-executing host and writes the hostname permanently into public Certificate Transparency logs - Personal-assistant pilot (Track C) — one-volunteer pilot including live memory/skill-poisoning tests against a real Hermes deployment Planned — not started; blocked on Track 0.2 landing first
- AI quality flywheel (Track 4) — capturing the human's verdict on AI output, and a curated "house style" knowledge base, so the drafts get better instead of staying whatever the first prompt produced Planned — design only, nothing built (
docs/ai-quality-flywheel.md). The central finding is that the signal already exists and is thrown away in two places: the PA's task proposal gets an explicit human yes/no and the rejection is written nowhere, and the scheduled status draft is edited by a human before saving, which the next day's tick can resolve at zero additional queries. So phase 1 is server-side only, adds no tokens and needs no front-end change. The document deliberately does not endorse the whole of itself: phase 4 — the prompt injection, and the only part that actually requires the flywheel premise — is gated behind a written baseline and a measured acceptance rate below ~80%, on the argument that at this company's volume a ten-point shift is at the edge of detectability; phases 1–3 are worth shipping whether or not phase 4 ever happens. Five items are left as open questions rather than assumptions, chiefly who curates the style base and whether one client's material may inform another client's output - PA blind-spot honesty (Phase 0.1 of
docs/agent-operating-model.md):PA_SYSTEM_PROMPT()now states what it can and can't see, derived from whatbuildContext()actually assembles, so it says "I can't see that" instead of guessing — the single cheapest idea taken from reviewing a competitor's product Shipped - Agent registry with five-gate readiness scoring (Phase 0.2): new
ai_agentstable +agents.html, every agent gets a stage/owner/SOP-owner and an Ownership/Documentation/Training/Evaluation/Deployment score, gatedrequireSuperadminOrLeadership; seeded with the 3 real agents (PA + status-suggestion live, knowledge-ask correctly marked planned) Shipped - Full entity model — departments/functions/workflows/SOPs as real tables, with agents bound to a workflow as an authorisation boundary, and only then the views (org graph, sprint planner, compliance reporting) that sit on top (Phase 1–3 of
docs/agent-operating-model.md) Planned — scoping decision (how many departments, who authors the workflow list) needed before any code - AI disclosure policy → a universal access model — what the assistant may disclose, first written as a matrix over our own four roles (
docs/ai-disclosure-policy.md) and then generalised into a five-layer, per-tenant model so it could hold a company whose roles are not ours: fixed knowledge classes, a locked floor no customer can lower, composable scope predicates, the tenant’s own effective-dated policy rows, and one enforcement path shared by the page and the assistant (docs/access-policy-model.md; review surface atdocs/access-model.html) Planned — design only: no policy engine exists, no table stores a policy row, and no route consults one. What has actually shipped out of it is Step 0, closing the two AI surfaces that disclosed more than the pages they mirror (8c513b4): the activity digest now carriesrequireSectionAccess(SECTION.ACTIVITY)— the same gate as the feed it summarises, where before a plain admin got 403 on the page and 200 on an LLM summary of the same rows — and conversation summarising is participant-only for DMs, with the admin bypass kept only for group threads. Four questions are left open for the owner and none can be settled by more design work, the first being whether a customer edits their own policy at all or we configure it at onboarding
DeepSeek APIHermes (Nous Research) — inertWhisper / STT (planned)Vector store (planned)
Phase 5
Scale, Mobile & Integrations
In progress- Theater / all-hands, recordings (Magicast) Planned
- Mobile app — Expo/React Native in
mobile/: Home as the dashboard (650793a), chat with paged threads and a measured keyboard offset (4a25f90), tasks, projects, leave, office presence, room voice, push; EAS build + App Store Connect submit config (mobile/docs/release.md) Shipped — not yet released to a store - Task description + attachments with drag-and-drop, paste and a lightbox, on the shared chat upload validator (
6223660,e409d12) Shipped - Several assignees per task, with board cards carrying the same controls as rows (
daa02cc); per-assignment attribution — who put each person on a task — recorded (604b806) and shown as a two-letter chip with short-form names; the same commit moved Archive / Mark deleted into a superadmin gear menu headed Manage project (390c4f8) Shipped - Co-presence at object level — who else has this project or task open, never which page anyone is on (
roam-presence.js,af96315,docs/copresence.md) Shipped — the mechanism; no page opts in yet, so nothing is visible untilprojects.htmlis wired - Stage-flow animation — a spark along the stage rail when a task moves, a completion flash when one finishes, and nothing that only decorates (
ad5a045) Shipped - Brands: the Business page reworked into Brands (
ebbfe3f), reorderable (2124949), and each brand's project count expanding into its project list (archived and deleted included, labelled) and linking through toprojects.html?brand=(22d23bd) Shipped - Project file library — a per-project store separate from chat (
223bc7d) Shipped (server side; upload UI next) - GitHub / Figma surfacing on the map; observability & autoscaling
- Per-project Miro boards (multi-board, restricted creator) & Google Calendar/Meet integration (per-member OAuth, encrypted tokens, auto-refresh, real Calendar API meeting scheduling) Shipped
- Platform hardening: app-wide auto-save on field-exit, site-wide error reference codes (ERR-XXXXXX), a Status page with uptime chart + downtime log, and two-way Clicking sync scaffolding Shipped
- Weekly billing summary: quotation/invoice/payment roll-up per project owner, notified to admins on a Monday 09:00 HKT schedule Shipped
- Per-project WIP-stage picker on
projects.html's task list, built from each project's ownwipStages(server-side validation already existed; the UI to see the stage a task sits on, and move it, didn't) Shipped - WIP-stage template editor — the stage list itself is now authorable, from the project's Details panel, gated to a project owner or admin/superadmin (matching the server's existing
OWNER_OR_ADMIN_FIELDScheck onwipStages). Until this,wipStageswas an API-only field: the column, the validation and the permission gate all existed, but nothing in the product could write it, so every project was stuck on the defaulttodo/doing/doneforever and a "WIP" stage was literally unreachable. Renaming a stage re-stamps the tasks sitting in it, removing one sends them to the new first stage, and both the template change and every task moved are audit-logged;doneis pinned last and locked, because the literal string is load-bearing in the server's done/status sync and itstaskDone/taskCountrollups. No server change was needed Shipped - Board view for a project's tasks — a List / Board toggle in the Tasks section, remembered per viewer; columns are the project's own WIP stages, and dragging a card between columns changes the task's stage Shipped
- Project status log grouped by deliverable — the log was one flat newest-first table even though
project_status_entries.deliverable_idhad been populated for a while; each deliverable is now its own group with status/due/assignee/description and an update count. Presentation only: no schema and no server change Shipped - Per-project view templates & a Deliverables view — the two project types (tech/web vs. creative) need different task layouts; designed in
docs/task-board-layouts.mdanddocs/project-templates.md, with a static, non-wired mockup atmockup/deliverables-view-preview.htmlbuilt from the real stylesheets Planned — design + mockup only, awaiting approval; nothing is wired and no schema or route change has been made - Notification hygiene: you are no longer notified about your own actions (a central gate in
notify()with one explicit, greppableallowSelfopt-out for the scheduled meeting reminder, which fans out to the whole team including the creator), and desktop/app pop-ups became an opt-in per category preference defaulting off — a second axis alongside the existing "should this notification exist at all" preference. Also resolved the duplicate pop-upschat.html/leave.htmlwere firing alongside the new watcher (docs/self-notifications.md,docs/desktop-notifications.md) Shipped - Patch-contract hardening: an unknown field in a task/project
PATCHis now a400on both backends instead of being silently dropped on Postgres and silently kept in memory (assertKnownPatchFields(), asserted first in all fourupdate*implementations), anddueAt/systemSource/recurParentIdnow write audit rows — they persisted, but were missing fromDIFF_FIELDS, so the deliberate no-op guard swallowed them Shipped
React NativeMiro APIGoogle Calendar APIClicking
Phase 6
Consistency & Office Authoring
Planned- Form-validation consistency pass — one shared validation/error pattern across every create/edit form (required fields, inline messages, disabled-until-valid) Planned — not yet built
- Office room-sizing + a 16×16-grid office layout editor — author room shapes/positions on a real grid, extending today's server-persisted room label/order/size/visibility overrides (
/api/org/room-layout) to shapes and positions Planned — not yet built
Vanilla JSCanvasPostgres
Phase 7
Security Hardening, Research & Platform Reuse
In progress- Three sequential Fable-5-model code reviews — server security, chat frontend, and projects/admin/shared frontend+deploy — found and fixed 2 CRITICAL, 5 HIGH, ~14 MEDIUM, and ~20 LOW/suspected findings across authentication, WebSocket authorization, XSS, rate-limiting, and cross-project data leaks; every finding fixed and verified Shipped
- Market-research comparison against ro.am, Gather, Kumospace, and other virtual-office tools, with per-area copy/skip/build recommendations for Roam (
docs/competitor-research.md,docs/roam-features.md) Shipped - iOS/Android mobile app strategy — the planning document, since superseded by the app itself (Phase 5 above;
mobile-guide.html) Shipped - Mac desktop shell — Electron source in
desktop/with a hotkey window for the assistant (1464e58,docs/mac-client.md) In progress — not one line run on a Mac; gated on whether a Chrome-installed PWA already answers the request (docs/open-decisions.mdD6) - CSS / design-system extraction — pulling
roam.css's tokens and shared components into a form reusable across other 5 Miles Lab platforms, not just this app In progress - Commercializing this platform for other companies — business/packaging plan Planned — not yet written
Fable 5Security reviewroam.cssDesign system
Phase 8
Project Cost & Xero
Planned- The goal: budget vs. actual-cost-to-date per project, shown next to the quotation/invoice checklist already in
projects.html's Finance tab, so a project's progress can be read as burn against budget rather than just "what have we billed" Planned — researched only, no code written (docs/xero-cost-integration.md) - What already exists to build on:
server/src/finance.js+project_revenue_items/project_expenses/project_finance_changes(persisted, change-logged, file bytes in Postgres), the Finance tab, the Monday-09:00-HKT billing summary, and this repo's settled third-party conventions — credentials viagetConfig(), a fail-softxEnabled(), astatus.jshealth check, tokens encrypted at rest likegoogle_accountsShipped - The catch, stated plainly: 5MLOS does not track project COST today — it tracks project REVENUE. Quotations, invoices and hand-entered third-party expenses exist; labour cost, time tracking, rate cards and budgets do not exist anywhere in the data model. This phase creates the concept, it doesn't wire up an existing number Planned
- Unresolved and blocking: does 5 Miles Lab actually use Xero Projects, and does anyone log time against jobs in it? Xero Projects is a paid add-on (from $7/month on Xero's HK pricing) and the whole integration returns empty panels if time isn't being logged and bills aren't coded to projects. Not answerable from the repo — it is a look-in-Xero question, and if the answer is no the real work is a bookkeeping habit, not code Open question
- What Xero's API actually returns (verified against Xero's own OpenAPI spec + developer.xero.com, not from memory): the Projects API has exactly seven endpoints, and every money field on a project —
totalTaskAmount,estimate,totalInvoiced,expenseAmountInvoiced— is a charge-out figure. Time entries carry minutes and a user id but no money;ProjectUsercarries no cost rate; there is no per-expense endpoint at all. Xero exposes no cost-to-us number, so real cost = minutes × a rate card we would have to hold ourselves (Xero Payroll is AU/UK/NZ only — there is no HK payroll to read salaries from) Planned - Auth constraint: Xero's clean machine-to-machine option (Custom Connections, client-credentials) is documented as available only to AU/NZ/UK/US organisations — 5 Miles Lab is Hong Kong, so this needs the ordinary OAuth code flow with
offline_access: a 30-minute access token and a 60-day refresh token that rotates on every use. That means an admin "connect / reconnect Xero" screen and a visible disconnected state, not a fire-and-forget env var. Rate limits (60/min, 1,000–5,000/day per tenant) are a non-issue for a nightly poll and a real problem for per-page-load fetches Planned - Join key: a Xero project has no external-reference field, so matching it to a 5MLOS project needs a mapping we store and a human sets once (name matching is too fragile) — a new change-logged column/table, plus a second mapping if the Accounting-API route is used, which permanently consumes one of the only two active tracking categories a Xero org gets Planned
- Phasing: (0) confirm in Xero that time is logged at all — hours, no code, and it decides everything; (1) read-only mirror —
integrations/xero.js, org-wide encrypted token store, scheduled pull cached in Postgres, a Finance-tab panel showing estimate / minutes logged / invoiced vs. to-be-invoiced, honestly labelled as charge-out, not cost; (2) actual cost, which needs a per-member cost-rate card in 5MLOS and an explicit decision on who may see margin — the Finance tab is visible to every project team member today; (3) two-way sync last, if ever, since it writes into the accounting system of record Planned - Posture note: project finance data currently leaves the server for nowhere — amounts and attached PDFs sit in Postgres in plaintext and the weekly summary is an in-app notification, not email. A Xero connection would be the first credential 5MLOS holds against the company's accounting system and the first outbound path for financial data; that is a deliberate change, not an inherited one Planned
Xero Projects APIXero Accounting APIOAuth 2.0Postgres
Tech Architecture
request & media flow, top → bottom🖥️Clientbrowser · mobile
Web app
React / Next.js, TypeScript
Office map
Canvas / SVG, Zustand state
Media client
WebRTC, getUserMedia
Mobile
React Native (later)
▼
🛡️EdgeTLS · routing
Traefik (Coolify proxy)
Let's Encrypt ACME, routes by Coolify labels
CDN / static
Nginx for app shell & assets
▼
⚡Realtimepresence & media
Presence gateway
WebSocket fan-out, heartbeats
SFU
LiveKit / mediasoup audio+video
TURN / STUN
NAT traversal for WebRTC
▼
🧩Application servicesREST / GraphQL
Core API
Node (NestJS) / Go · auth, orgs, rooms
Chat service
AInbox messaging & threads
AI service
Transcription & summary jobs
▼
🗄️Data & statepersistence
Postgres
Users, orgs, rooms, messages
Redis
Presence, pub/sub, sessions
S3
Recordings, uploads, assets
OpenSearch
Chat & minutes search
▼
🤖AIMagic Minutes · On-It
Speech-to-text
Whisper / streaming STT
DeepSeek API
Concierge, leave assistant, activity digest (live today)
Vector store
Retrieval over meeting history
☁️Infrastructure & deliverycross-cutting
Coolify host (arm64)
Shared Docker Compose host, 22 resources
ECR
Container registry
GitHub Actions
Build → push → Coolify
restart?latest=trueCloudWatch
Logs & metrics
Roadmap is presence-first: ship the live map (Phase 1) before media (Phase 2), since presence is the signature feature and the hardest realtime piece. Stack choices are indicative and can flex (e.g. LiveKit vs. mediasoup) as load and team familiarity dictate.