Skip to content
Pilotoo
v0.1

Multi-tenant SaaS

You will turn a single server into a multi-tenant one: the tenant comes from your identity service, each tenant gets its own skills, and chosen roles may manage agents, schedule runs and read usage. Everything is one forRoot call and a handful of environment variables.

src/app.module.ts
import { Module } from '@nestjs/common';
import { PilotooModule } from '@pilotoo/server';
import { appIdentityResolver } from './identity';
@Module({
imports: [
PilotooModule.forRoot({
env: process.env,
serviceName: 'assistant',
identityResolver: appIdentityResolver,
identityHeaders: ['x-tenant-id'],
skillLayers: {
globalDir: process.env.AGENT_GLOBAL_SKILLS_DIR,
tenantDir: process.env.AGENT_TENANT_SKILLS_DIR,
},
configTools: { profiles: false, skills: true },
scheduleTools: true,
}),
],
})
export class AppModule {}
  • identityHeaders copies those request headers to the identity resolver. Use it when your identity service picks the tenant by header. Header names are matched lower-case.
  • skillLayers adds two skill folders (see step 2). Both fields also default to the environment variables of the same names, so you can leave skillLayers out.
  • configTools gives agents tools to read and save profiles and skills. Here only skills.
  • scheduleTools registers schedule_create, schedule_list, schedule_update and schedule_delete, opt-in.
skills/ AGENT_SKILLS_DIR shipped with your app (system layer)
global-skills/ AGENT_GLOBAL_SKILLS_DIR put by hand, for every tenant, read only
tenant-skills/
acme/ AGENT_TENANT_SKILLS_DIR one folder per tenant
refund-policy/SKILL.md
globex/
refund-policy/SKILL.md

On a shared name, tenant wins over global, and global over system. A tenant’s callers list, run, read and write only their own tenant folder.

Roles come from the identity. Each variable is a comma-separated list; unset means nobody.

.env
# Who manages agents, and (with configTools) skills
AGENT_MANAGER_ROLES=permission:assistant.manage
# Who may schedule runs as themselves, and who may also manage the whole tenant's schedules over the REST routes
SCHEDULE_ROLES=permission:assistant.schedule
SCHEDULE_ADMIN_ROLES=permission:assistant.manage
# Who reads the whole tenant's usage
USAGE_ADMIN_ROLES=permission:assistant.manage
# Where the server finds its files, pinned so they do not depend on where it is installed
PILOTOO_CONFIG_DIR=/srv/assistant/.pilotoo
AGENT_SKILLS_DIR=/srv/assistant/skills
AGENT_WORKSPACE_DIR=/srv/assistant/.workspace
AGENT_GLOBAL_SKILLS_DIR=/srv/assistant/global-skills
AGENT_TENANT_SKILLS_DIR=/srv/assistant/tenant-skills

The role names are whatever your identity resolver returns in roles. The example uses the permission:<code> convention from the sign-in recipe.

Schedule tools are opt-in, so name them in the agent.

.pilotoo/agents/planner.yaml
id: planner
name: Planner
description: Sets up recurring reports.
status: active
runtime:
systemPrompt: You help the user schedule recurring tasks.
profileId: default
tools: [schedule_create, schedule_list, schedule_update, schedule_delete]
builtinTools: []
skills: []
knowledge: []
presentation: {}

An agent with tools: null gets neither the schedule tools nor the config tools.

  • Isolation follows the tenant of the identity: conversations, attachments, tenant skills and usage are all keyed by it.
  • The schedule tools act on the caller’s own schedules only, whatever their role. A caller without a schedule role gets a “Not done” answer from the tool, not a silent no-op.
  • configTools with skills: true writes only to the caller’s tenant folder when a tenant layer exists.