Skip to content

🎬 Mastra Studio β€” Driving Agents as a Logged-In User (ENG-1096) ​

How to attach Mastra Studio to the backend's mounted /mastra API and drive the onboarding / workspace-assistant agents locally as a chosen (impersonated) user + company, instead of the anonymous default.

Why ​

Studio's dev playground sends no auth header β€” it can't produce a real Firebase-verified user. Agent tools need { user, companyId } to do anything useful (read/write company-scoped data), so getActingIdentity (apps/agri-backend/src/mastra/mastra-context.ts) accepts a presetactingUserId / activeCompanyId pair from Studio's request context as a stand-in for a verified user β€” but only when NODE_ENV === 'local'. Every other environment must authenticate for real.

1. Start the backend in local mode ​

bash
# apps/agri-backend/.env
NODE_ENV=local
PORT=3888
bash
pnpm --filter agri-backend dev
# or the "πŸƒ Backend" VS Code debug config

Confirm it's up:

bash
curl http://localhost:3888

With NODE_ENV=local, AppGuard (apps/agri-backend/src/app.guard.ts) bypasses auth entirely for any /mastra* route β€” this is what lets Studio attach without a token. In every other NODE_ENV value (including development, which is a deployed value, not local), /mastra requests fall back to the normal x-api-key / Firebase auth path like any other route.

2. Run Mastra Studio pointed at the backend ​

Studio runs its own local UI server (default http://localhost:3000) and proxies agent/workflow calls to a target Mastra instance you point it at via the --server-* flags β€” here, the backend's mounted /mastra API on :3888:

bash
pnpm dlx mastra@latest studio \
  --server-host localhost \
  --server-port 3888 \
  --server-protocol http \
  --server-api-prefix /mastra

Open http://localhost:3000 β€” Studio's UI. It talks to the instance at http://localhost:3888 under the /mastra prefix, which is where the @mastra/nestjs adapter is mounted (apps/agri-backend/src/mastra/mastra.module.ts).

3. Impersonate a user + company with a presets file ​

Studio lets you seed each request's RequestContext from a JSON file via --request-context-presets. Create one locally (gitignored, don't commit it):

json
// mastra-presets.json
{
  "actingUserId": "<a dev user _id>",
  "activeCompanyId": "<their company _id>"
}
bash
pnpm dlx mastra@latest studio \
  --server-host localhost \
  --server-port 3888 \
  --server-protocol http \
  --server-api-prefix /mastra \
  --request-context-presets ./mastra-presets.json

getActingIdentity reads actingUserId / activeCompanyId off the request context, loads the user via the registered userService (apps/agri-backend/src/mastra/mastra-services.ts), validates the user actually belongs to that company (UserUtils.validateCompanyAccess), and hands tools a real { user, companyId } β€” same shape the authed path produces from a verified Firebase user.

IMPORTANT

Preset impersonation is hard-gated to NODE_ENV === 'local' in getActingIdentity itself, independent of the AppGuard bypass above. If either check fails, tools throw No acting identity: authenticate, or set actingUserId/activeCompanyId presets in local dev instead of silently running as nobody.

Getting valid actingUserId / activeCompanyId values ​

Use ids from your local database (seeded via ./dev/scripts/create-user-db.sh, see Local Environment):

  • Connect with MongoDB Compass (mongodb://user:pass@localhost:27017/?authSource=admin&directConnection=true).
  • Pick any document in users β€” its _id is actingUserId.
  • Use one of its memberships[].companyId values as activeCompanyId (the user must actually hold that membership β€” validateCompanyAccess rejects mismatches).

Alternatively, register a fresh account through the locally running frontend and look it up the same way once it lands in Mongo.

Deployed admin impersonation ​

Shipped, flag-gated (default off). On staging/production the Studio surface is mounted only when MASTRA_STUDIO_ADMIN_ENABLED=true, and every /mastra request is authenticated against a real Firebase token β€” never the local preset path.

Flow:

  1. Mount gate (mastra.module.ts): /mastra is registered only when isLocalEnv() or isStudioAdminEnabled(). Off β†’ the routes don't exist.
  2. Strip client identity (mastra-strip-identity.middleware.ts): on non-local envs the middleware deletes any caller-supplied identity keys from req.body.requestContext, so identity can only come from the verified token.
  3. Token verification (mastra-auth.ts β†’ adapter server.auth): the bearer Firebase ID token is decoded and resolved to its app user, which the adapter sets as the tool identity. No token β†’ adapter 401.
  4. Admin gate (mastra-auth.ts): the resolved caller must be a SUPER_ADMIN, or the token must carry an impersonatedBy claim (an admin-originated impersonation session β€” see impersonation.controller.ts). A normal user's own token is rejected even though the identity-scoping invariant would already confine it to that user's data.
  5. Getting the token: the flag-gated CopyStudioTokenButton (frontend, shown while impersonating) copies the current session's Firebase ID token; paste it into Studio β†’ Add Header β†’ Authorization: Bearer <token>.

Net invariant: no deployed /mastra request runs a tool as an identity not proven by a verified token, and the surface is reachable only by admins / admin-originated impersonation sessions.