fleetlessfleetlessdocs
Recipes/App Starter

🧱 App Starter

Every screen an app user can reach, already built, on @fleetless/sdk and the Nuxt UI dashboard. Clone it, point it at an app, and the robots a role grants appear.

Sign-in by password or emailed code, two-factor (the challenge, recovery codes, the setup an app can require, and turning it on or off under Account › Security), registration, verification, invitations, password reset, single sign-on and the MCP consent screen; the robots the signed-in person reaches; and on each robot every datapoint, action, service, publisher, camera, run history and asset their role grants. It is MIT, at github.com/fleetless/app-starter, and it is a GitHub template repository.

What a screen shows is decided by the person’s role in the console, re-read on every call. A role that grants nothing sees a polite empty page; a role that grants everything sees it all. The app does not decide, it draws.

Run it

Node 22 and pnpm (corepack enable), then three commands:

sh
pnpm install
cp .env.example .env      # fill in NUXT_PUBLIC_APP_IDENTIFIER
pnpm dev

NUXT_PUBLIC_APP_IDENTIFIER is the app’s identifier from the console, NUXT_PUBLIC_API_URL is the cloud it talks to, and NUXT_DEV_ALLOWED_HOSTS is the comma-separated list of DNS names the dev server may answer to. The shipped NUXT_PUBLIC_API_URL points at a local dev stack, so set it to https://api.fleetless.dev unless you run one — a cloud that answers nothing fails the way a missing origin does, as a network error with no status.

Leave NUXT_DEV_ALLOWED_HOSTS empty unless you reach the dev server by a DNS name, through a tunnel or from another machine on the network. Vite refuses a host header it was not told about, which is what stops a DNS-rebinding attack on your dev server; localhost and plain IP addresses need no entry.

Set it up in the console

An app in the console is what this template draws; see Create an App for what an app is. Five settings, in this order. Each names the refusal you see without it, so you can tell a missing setting from a broken app.

  1. Create an app on the Apps page. Its identifier is lowercase letters, digits and underscores, starting with a letter, and it goes into .env as NUXT_PUBLIC_APP_IDENTIFIER. Without it, register, password reset and the provider list answer 404 not_found, and sign-in answers invalid_credentials like every other miss.

  2. Registration page, allowed_origins: the origin the app runs on, bare — scheme, host and port, no path, no trailing slash. Without it, every call fails as a network error with no status and no body, because the browser refuses the answer before your code sees it.

    Optional, once the app runs somewhere the people you invite can reach: on the app’s Sign-in page, under Pages, switch Invitation, Email verification, Password reset and MCP sign-in to Your app with these URLs. Until then Fleetless’s hosted pages answer every mailed link and the MCP sign-in (see Create an App).

    Setting Value
    verify_url <origin>/auth/verify/{token}
    reset_url <origin>/auth/reset/{token}
    invite_url <origin>/auth/invite/{token}
    mcp_login_url <origin>/mcp/{interaction}, with mcp_enabled on
  3. Settings page, default_role_id: the role a self-registered account gets. A new app has none. Without it, registration answers 409 target_state_conflict naming the field, and a federated callback answers no_access.

  4. Roles page: tick the datapoints, actions, services, publishers and cameras the role grants, plus the action_history and assets capabilities if the Activity and Assets tabs should exist. A new role grants nothing. Without a grant, sign-in works and the robot list is empty.

  5. Attach a robot to the app on its page, then create or invite an app user on the Users page. That user signs in here with a password or an emailed code, whichever the app’s Sign-in page turns on (password by default). If it requires two-factor, the starter sets it up at the first sign-in. Your own console login will not, and answers invalid_credentials when you try: it is a Fleetless user, a different identity space — see Manage Users and Roles.

Then sign in. The robot appears; click it.

Or let an agent do it

The five steps above are also ten tool calls, eleven when the mailed links should open in the app, one more per extra robot. Paste the prompt below into an MCP client connected to the central endpoint, https://mcp.fleetless.dev, signed in as a Fleetless user of your organisation. Anything that speaks MCP will do.

The block is the starter’s own AGENT-SETUP.md prompt, with item ©'s default origin spelled out in words rather than as a literal address. Step 4 grants presence, a switch nothing consults yet and harmless to set.

text
You are setting up a Fleetless app for the app-starter template using the Fleetless MCP tools on the central endpoint. Work through the steps in order, call the tools rather than describing them, ask me only the questions in step 1, and invent no values. If a tool refuses, stop, quote the code and the sentence it returned, and ask me how to continue.

1. Ask me for, in one message:
   (a) the app name;
   (b) the app identifier — lowercase letters, digits and underscores, starting with a letter;
   (c) the origin the app runs on, defaulting to the one the dev server prints when it starts;
   (d) which robots to attach — call console_robots_list first and show me their names;
   (e) the email address of the first app user;
   (f) whether Fleetless should mail the invitation, or hand me the link;
   (g) whether mailed links and the MCP sign-in should open in this app instead of on Fleetless's hosted pages — default no; yes only once the app runs at an origin the people I invite can reach.

2. Create the app with console_app_create: name, identifier and the chosen robot_ids. Keep the app id.

3. Read the auth configuration with console_app_auth_config_get, to see the current self_registration, allowed_domains and app_url. Then write each slice, carrying every field that slice takes:
   - console_app_auth_config_registration_put: self_registration and allowed_domains exactly as read, allowed_origins set to the origin from (c), bare — scheme, host and port, no path, no trailing slash
   - console_app_auth_config_mcp_put: mcp_enabled true
   - only if I said yes in (g), console_app_auth_config_urls_put: app_url exactly as read, invite_url <origin>/auth/invite/{token}, verify_url <origin>/auth/verify/{token}, reset_url <origin>/auth/reset/{token}, mcp_login_url <origin>/mcp/{interaction}; otherwise leave the URLs unset, and Fleetless's hosted pages answer those links

4. Create a role named "Operator" with console_role_create and keep its id. For every attached robot call console_robot_exposures and collect its slugs. Then call console_role_permissions_put once, with one grant per robot carrying all of that robot's slugs, and the capabilities action_history, presence and assets all true. A robot with no exposures gets no grant; note it for the report.

5. Make that role the app's default with console_app_update, default_role_id.

6. Invite the first user with console_app_user_invite: the email from (e), role_id of the Operator role, send_mail true if I chose mail in (f), otherwise false.

7. Report, and nothing else:
   - the line for .env: NUXT_PUBLIC_APP_IDENTIFIER=<identifier>
   - the invitation: "mailed to <email>", or the accept_url exactly as returned
   - whether the mailed links open in this app or on Fleetless's hosted pages
   - every robot that was attached and what it was granted, and any robot that exposes nothing
   - anything that was refused, with the code and the sentence

The agent asks seven things once and does the rest, then reports the .env line and the invitation. It cannot choose a password, because the invitation is where the person sets one when the app signs in by password, and it cannot grant what a robot does not expose. A robot whose configuration has no datapoints, actions, services, publishers or cameras is attached, granted nothing, and named in the report.

Make it yours

  • app/app.config.ts — the app’s name, its description, the four brand files and the primary colour.
  • public/brand/ — four files, replaced in place; nothing reads them by another name. icon-light.svg and logo-light.svg are dark ink for a light ground, icon-dark.svg and logo-dark.svg light ink for a dark one, and the page picks by theme because an image takes no colour from the document around it. The favicon is the dark-ground icon for both tab strips.
  • A page added under app/pages/ is covered by the auth guard already. A tab on the robot page comes from extending tabsFor in app/utils/datasheet.ts.
  • The robot page’s 3D tab renders the robot’s synced URDF, textures and .dae-embedded images included, with client.assets.prepareUrdfScene and three.js with urdf-loader. It moves the joints from the joint_states datapoint when the role grants it, and a robot without a synced URDF shows a notice instead. The component is ModelView.client.vue.

What is where

Path What
app/plugins/fleetless.client.ts The one SDK client, with a localStorage token store.
app/composables/useSession.ts Who is signed in, and where an expired session goes.
app/middleware/auth.global.ts Everything but /auth/* and /mcp/* needs a session.
app/pages/auth/* Login, register, verify, forgot, reset, invite, the OIDC callback.
app/composables/useSignInResult.ts Where every sign-in result goes: signed in, or on to the second factor.
app/pages/auth/two-factor/* The second factor: the code or a recovery code, and the setup an app can require.
app/pages/account/security.vue Two-factor on or off, from the user menu’s Security.
app/pages/mcp/[interaction].vue The consent screen an AI tool sends its user to.
app/pages/robots/* The list, and the robot page whose tabs come from the datasheet.
app/components/robot/ParameterForm.vue A form built from a datasheet’s input_schema.
app/utils/errors.ts Every refusal’s one sentence.

pnpm lint && pnpm typecheck && pnpm test && pnpm build is the check set, and pnpm generate writes a static dist/ any web server serves. The cloud is the only backend. The SDK reference documents everything this app calls.