π¬ 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 β
# apps/agri-backend/.env
NODE_ENV=local
PORT=3888pnpm --filter agri-backend dev
# or the "π Backend" VS Code debug configConfirm it's up:
curl http://localhost:3888With 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:
pnpm dlx mastra@latest studio \
--server-host localhost \
--server-port 3888 \
--server-protocol http \
--server-api-prefix /mastraOpen 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):
// mastra-presets.json
{
"actingUserId": "<a dev user _id>",
"activeCompanyId": "<their company _id>"
}pnpm dlx mastra@latest studio \
--server-host localhost \
--server-port 3888 \
--server-protocol http \
--server-api-prefix /mastra \
--request-context-presets ./mastra-presets.jsongetActingIdentity 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_idisactingUserId. - Use one of its
memberships[].companyIdvalues asactiveCompanyId(the user must actually hold that membership βvalidateCompanyAccessrejects 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:
- Mount gate (
mastra.module.ts):/mastrais registered only whenisLocalEnv()orisStudioAdminEnabled(). Off β the routes don't exist. - Strip client identity (
mastra-strip-identity.middleware.ts): on non-local envs the middleware deletes any caller-supplied identity keys fromreq.body.requestContext, so identity can only come from the verified token. - Token verification (
mastra-auth.tsβ adapterserver.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. - Admin gate (
mastra-auth.ts): the resolved caller must be aSUPER_ADMIN, or the token must carry animpersonatedByclaim (an admin-originated impersonation session β seeimpersonation.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. - 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.