Skip to content

App Surfaces

Daniel Hokanson edited this page Aug 30, 2026 · 1 revision

Forge presents as one web application, and physically it is one: a single Angular bundle, served by a single nginx container, in front of a single API. But that bundle serves six distinct entry points with different chrome, different audiences and — in three cases — a different way of proving who you are. Knowing which is which is the difference between "the app looks broken on my phone" and "a phone is redirected to the phone UI on purpose". This page is the map: what the surfaces are, how a user is routed to one, and what determines the menu they see once they are there.

UI Flows documents the screens inside the desktop employee shell. This page is about everything around it.

The six entry points

Path Surface Who it is for How it authenticates
/ Employee shell — header, drill-down sidebar, main pane Office and management staff on a desktop A Forge user account
/m Phone web UI — a worker-shaped view with clock, jobs, scan, hours, chat, notifications and account Anyone on a phone browser A Forge user account
/app Native shell — the Capacitor build's own screens (scan, clock, stock move, lookup, jobs, account) Phones and handhelds running the installed app An enrolled device, then a person; a device marked shared carries no user session between transactions
/display/shop-floor Kiosk display, plus a team clock and inventory-scan variant A terminal bolted to the floor A device token minted at pairing; workers identify per action
/portal Customer portal — its own layout, no employee chrome Your customers' contacts A passwordless magic link, exchanged for a portal-only session
/accept/:token Single-purpose page to review and accept one sales order A customer following an emailed link Nothing — the link's token addresses the order

Alongside these sit the ordinary account routes (/login, the SSO callback, /setup/:token for a new employee completing their own password and PIN) and /app/enroll, which is deliberately outside the device guard so an unenrolled phone can reach the enrolment screen.

Because all six are the same origin and the same bundle, there is nothing to split at the reverse proxy: one upstream serves the lot. The only requirement is the standard SPA fallback — unknown paths must return index.html, which the shipped nginx configuration already does. What you should decide deliberately is exposure: the last two rows answer without a Forge login, and both are capability-gated so an install that does not want them can switch them off. See Hardening a Production Install before putting Forge in front of the internet, and Installation for the deployment-target choice.

A phone is routed away from the desktop app

The employee shell is guarded: a touch device on a phone-sized viewport is redirected to /m. Detection is deliberately forgiving — it measures the shorter of the two viewport dimensions so a landscape phone still counts, falls back to the user agent for awkward cases, and treats an installed standalone PWA as a phone regardless of size.

There is one opt-out. The phone UI's account screen offers "view desktop site", which sets a per-tab preference; while it is set the guard lets that tab through to the desktop shell. It is session-scoped on purpose — close the tab and the phone gets the phone UI back. Two desktop areas are exempt from the redirect entirely, /account and /onboarding, because they read fine on a small screen and a worker may be sent to them by a link.

A desktop touchscreen monitor mounted on the floor is the case to watch: it can satisfy the touch test. If a large touch display is landing on /m when you wanted the kiosk, point it at /display/shop-floor directly rather than at the root — see Shop Floor Kiosk.

Where you land after signing in

The root path is not a page; it is a resolver. It runs in this order, and stops at the first answer:

  1. Native shell build — always /app.
  2. A phone/m, by the detection above.
  3. An explicit landing preference, chosen by the user in Account → Customization. Setting it back to automatic clears the preference and re-enables the step below.
  4. A role-based landing, but only for a user holding exactly one role.
  5. The dashboard, for everyone else.

The role map sends a single-role user to the screen their day actually starts on:

Role Lands on
Production Worker, Production Manager The board
Production Planner Planning
Engineer Parts
PM Backlog
Procurement Purchasing
Controller Accounting
Office Manager Customers
Compliance Officer Compliance

Admin, Manager and IT Admin are deliberately absent from that map — their access is too broad for any one screen to be the right guess, so they land on the dashboard. So does anyone holding two or more roles: the rule is single-role, and a second role reverts the user to the dashboard rather than picking a winner. Landing targets are best-effort; if a mapped screen is gated off by capability for this install, its own guard falls the user back to the dashboard. An unknown URL is sent back through the same resolver rather than rendering an empty shell.

The sidebar is a drill-down tree, not a flat list

The employee shell's navigation is a two-tier tree. The dashboard sits as a top-level peer, and everything else groups by the value streams a manufacturer recognises rather than by internal module boundaries: Sales, Production, Purchasing, Inventory, Equipment, Fulfillment, Financials, Quality, People, Insights, with the shop-floor display and the admin area pinned below. Selecting a group drills into its leaves; several leaves are themselves groups one level deeper.

Group headers are organisational, not routable. Typing a group name into the address bar redirects to that group's first routable page — /sales lands on Customers, /production on Parts — so a guessed URL always lands somewhere sensible instead of on a blank shell. Breadcrumbs resolve the same way, which is why an intermediate crumb is clickable.

The important part for operators: every nav entry carries an optional capability and an optional role list, and the whole tree is filtered per user before it renders. Two installs of the same image show different sidebars, and two people on one install show different sidebars. Two filtering behaviours follow from that and surprise people:

  • A group whose children are all filtered out disappears entirely rather than opening into nothing.
  • A group reduced to a single surviving child promotes that child up a level, so you do not drill through a one-item shell.

The consequence is that "the menu item isn't there" is almost never a bug — it is a capability that is off or a role the user does not hold. Capability Gating explains which is which and where to change it. The same filtered tree feeds the header's search-by-page, so a page you cannot see in the sidebar is also not findable by search.

The customer portal

/portal is a genuinely separate application surface sharing the same bundle. It has its own layout with no employee sidebar, its own session, and no employee role check in either direction — an employee session grants nothing inside the portal, and a portal session grants nothing in the employee app. The shell chrome is suppressed on portal routes even in a browser that is simultaneously signed in as staff, so a salesperson checking what a customer sees is not shown a half-employee page.

Sign-in is passwordless. The contact enters their email address, the server mails a one-time link, and following it exchanges that link for a short-lived portal session carrying only that customer and contact. Inside, the contact gets a dashboard plus their sales orders, quotes, invoices and shipments, and can accept or decline a quote. Only the two sign-in endpoints answer anonymously; everything else on the portal requires the portal session.

Access is granted per contact, from Customers → Portal Access in the employee app, and an admin can revoke a contact's access without deleting the contact. The whole surface sits behind CAP-EXT-CUSTOMER-PORTAL: with the capability off, the routes stop answering at all. Two practical prerequisites — the portal needs working outbound mail (no mail, no magic link) and a deployment target your customers can actually reach, which is a decision, not a default.

/accept/:token is the smaller sibling: a single sales order, reviewed and accepted from an emailed link with no portal account at all. Treat both as external surfaces and read Hardening a Production Install for what to switch off, what to narrow, and what else answers without a Forge login.

What differs per surface, and what does not

Language, wording and branding are install-wide and follow the user across surfaces — see Customizing an Install. The one deliberate exception is the kiosk, which keeps its own theme and font size per terminal so a bright shop floor can run light while the office runs dark.

Capability gating applies everywhere, but through different mechanisms: route guards and the nav filter in the employee shell, per-screen guards in the native app, and server-side refusals underneath all of them. A client of your own gets the same refusals over HTTP — see API Access for the shape of a capability refusal, and Configuration and Integrations for why a capability being on does not by itself mean the feature is wired up.

Related pages: Shop Floor Kiosk for pairing a terminal, Mobile and Offline for getting Forge onto phones and what actually works without a network, Architecture for what sits behind all six surfaces.

Clone this wiki locally