Aegis Multi-Tenancy Enforcer

VerifiedSafe

Ensures every MongoDB query includes communityId, sourced from the JWT token rather than the request body. Use this skill when writing queries, repository methods, or service logic in Aegis to prevent cross-community data breaches.

Sby Skills Guide Bot
DevelopmentIntermediate
6/2/2026
Claude CodeCursorWindsurf
#multi-tenancy#mongodb#security#nestjs#aegis

Recommended for

Automated summary

This skill enforces strict multi-tenancy by ensuring every MongoDB query includes `communityId` from the JWT token, preventing cross-community data leaks.

Strengths

  • Clear guidance on extracting communityId from JWT and passing it through controller, service, and repository layers.
  • Comprehensive examples of correct patterns for find, update, delete operations and indexes.
  • Explicitly forbids common anti-patterns like global queries, optional communityId, or trusting the request body.

Limitations

  • Does not handle legitimate cross-community access (e.g., admin roles) or provide fallbacks.
  • Assumes communityId is always present in JWT without covering validation or error handling.
  • Lacks discussion on performance implications or index optimization beyond the first field rule.
When to use it

When building or modifying NestJS services, repositories, or MongoDB queries in a multi-tenant application requiring data isolation per community.

When not to use it

For single-tenant applications, public APIs without community scoping, or when authentication does not rely on JWT tokens.

Security analysis

Safe
Quality score95/100

This skill provides code-level guidelines for enforcing multi-tenancy in MongoDB queries. It does not instruct any dangerous operations like file deletion, network calls, or data exfiltration. It is purely instructional text with example TypeScript code.

No concerns found

How this analysis is made

Examples

Create a multi-tenant repository method
Write a repository method for Payment model that finds payments by household ID, ensuring multi-tenancy by including communityId from the JWT context.
Fix a missing communityId query
The following repository method lacks multi-tenancy enforcement: `async findAll() { return this.model.find({}); }`. Rewrite it to include communityId from the token.
Add communityId to an existing index
This Payment schema index only has `householdId`. Update it to be a compound index with communityId first for query efficiency.

name: tenancy-enforcer description: Use this skill when writing MongoDB queries, repository methods, or service logic in Aegis. It enforces strict multi-tenancy by ensuring communityId is always included.

Aegis Multi-Tenancy Enforcer

When This Skill Applies

  • Writing ANY MongoDB query (find, update, delete)
  • Creating repository methods
  • Implementing service layer logic
  • Adding new indexes to schemas
  • Creating aggregate pipelines

The Absolute Rule

Every database query MUST include communityId in the filter.

No exceptions. Cross-community access is a security breach.

Source of communityId

Correct: From JWT Token

// Controller extracts from token
@Get()
async findAll(@CurrentUser() user: JwtPayload) {
  return this.service.findAll(user.communityId);
}

// Service passes to repository
async findAll(communityId: string) {
  return this.repository.findAll(communityId);
}

// Repository includes in query
async findAll(communityId: string) {
  return this.model.find({
    communityId: new Types.ObjectId(communityId),
  });
}

Wrong: From Request Body

// NEVER DO THIS
@Post()
async create(@Body() dto: CreateDto) {
  // dto.communityId could be forged by attacker
  return this.service.create(dto.communityId, dto);
}

Repository Method Patterns

Find Methods

// Always require communityId as parameter
async findById(id: string, communityId: string): Promise<Document | null> {
  return this.model.findOne({
    _id: new Types.ObjectId(id),
    communityId: new Types.ObjectId(communityId), // REQUIRED
  });
}

async findByHousehold(householdId: string, communityId: string): Promise<Document[]> {
  return this.model.find({
    householdId: new Types.ObjectId(householdId),
    communityId: new Types.ObjectId(communityId), // REQUIRED
  });
}

Update Methods

async updateById(
  id: string,
  communityId: string,
  data: UpdateDto,
): Promise<Document | null> {
  return this.model.findOneAndUpdate(
    {
      _id: new Types.ObjectId(id),
      communityId: new Types.ObjectId(communityId), // REQUIRED in filter
    },
    { $set: data },
    { new: true },
  );
}

Delete Methods

async deleteById(id: string, communityId: string): Promise<boolean> {
  const result = await this.model.deleteOne({
    _id: new Types.ObjectId(id),
    communityId: new Types.ObjectId(communityId), // REQUIRED
  });
  return result.deletedCount > 0;
}

Common Anti-Patterns (FORBIDDEN)

Global Queries

// WRONG - No communityId filter
async findAll(): Promise<Document[]> {
  return this.model.find({}); // SECURITY BREACH
}

// WRONG - ID-only lookup
async findById(id: string): Promise<Document | null> {
  return this.model.findById(id); // Can access ANY community's data
}

Optional communityId

// WRONG - communityId should never be optional
async findById(id: string, communityId?: string): Promise<Document | null> {
  const filter: any = { _id: new Types.ObjectId(id) };
  if (communityId) {
    filter.communityId = new Types.ObjectId(communityId);
  }
  return this.model.findOne(filter); // Dangerous when omitted
}

Trusting Request Body

// WRONG - Request body can be forged
@Post()
async create(@Body() dto: CreatePaymentDto) {
  return this.paymentService.create({
    ...dto,
    communityId: dto.communityId, // Attacker can set any communityId
  });
}

// CORRECT - Use JWT context
@Post()
async create(
  @Body() dto: CreatePaymentDto,
  @CurrentUser() user: JwtPayload,
) {
  return this.paymentService.create({
    ...dto,
    communityId: user.communityId, // From authenticated token
  });
}

Index Patterns

All compound indexes MUST have communityId as the first field for query efficiency and logical isolation.

// CORRECT - communityId first
@Schema()
export class Payment {
  // ...
}

PaymentSchema.index({ communityId: 1, householdId: 1 });
PaymentSchema.index({ communityId: 1, state: 1 });
PaymentSchema.index({ communityId: 1, householdId: 1, billingPeriodStart: 1 }, { unique: true });

// WRONG - communityId not first
PaymentSchema.index({ householdId: 1, communityId: 1 }); // Query won't use index efficiently
PaymentSchema.index({ state: 1 }); // Global index, no tenant isolation

Aggregate Pipeline Rules

// CORRECT - $match with communityId as first stage
async aggregateByHousehold(communityId: string) {
  return this.model.aggregate([
    {
      $match: {
        communityId: new Types.ObjectId(communityId), // FIRST stage
      },
    },
    {
      $group: {
        _id: '$householdId',
        total: { $sum: '$amount' },
      },
    },
  ]);
}

// WRONG - Missing communityId or not first
async aggregateAll() {
  return this.model.aggregate([
    { $group: { _id: '$householdId', total: { $sum: '$amount' } } }, // No tenant filter
  ]);
}

Service Layer Enforcement

Services should always require communityId from controllers, never have default values or lookups.

// CORRECT - Explicit communityId parameter
@Injectable()
export class PaymentService {
  async findByHousehold(householdId: string, communityId: string) {
    return this.repository.findByHousehold(householdId, communityId);
  }
}

// WRONG - Fetching communityId from related entity
@Injectable()
export class PaymentService {
  async findByHousehold(householdId: string) {
    const household = await this.householdService.findById(householdId);
    // What if household doesn't exist? What if wrong community?
    return this.repository.findByHousehold(householdId, household.communityId);
  }
}

Exception: Community Collection

The Community collection itself does not have a communityId field - it IS the tenant root. When querying communities:

// Community queries use _id directly (admin operations only)
async findCommunityById(id: string): Promise<Community | null> {
  return this.communityModel.findById(id);
}

Verification Checklist

Before committing any repository/service code:

  • [ ] Every find() call includes communityId in filter
  • [ ] Every findOne() call includes communityId in filter
  • [ ] Every updateOne()/updateMany() includes communityId in filter
  • [ ] Every deleteOne()/deleteMany() includes communityId in filter
  • [ ] All aggregate pipelines have $match with communityId as first stage
  • [ ] communityId comes from JWT/controller, not request body
  • [ ] No methods have optional communityId parameter
  • [ ] New indexes have communityId as first field in compound indexes

From the file to a first result

The download contains exactly the content displayed on this page. This copy may differ from the latest version in the source repository.

View the source and supporting files

This download contains SKILL.md only. Any scripts, references, templates and dependencies are not included. Get the required files from the source, preserving their directory structure, before continuing.

1. Download the file

Download SKILL.md

From the root of a test project, save the file as .skill-downloads/aegis-multi-tenancy-enforcer-mmio70nb/SKILL.md, or use the command below. This folder is for inspecting the skill before installation.

Command for macOS/Linux or WSL: requires a POSIX shell and curl. On Windows without WSL, use the download button and create the folder manually.

mkdir -p '.skill-downloads/aegis-multi-tenancy-enforcer-mmio70nb' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/aegis-multi-tenancy-enforcer-mmio70nb/download' --output '.skill-downloads/aegis-multi-tenancy-enforcer-mmio70nb/SKILL.md.part' && mv '.skill-downloads/aegis-multi-tenancy-enforcer-mmio70nb/SKILL.md.part' '.skill-downloads/aegis-multi-tenancy-enforcer-mmio70nb/SKILL.md'

2. Check prerequisites

Read SKILL.md before using it. If it references scripts or external services, inspect them and prepare the required access in your test environment.

No recognized prerequisites section in this file. This does not mean it is self-contained: libraries, tools and access requirements may be mentioned elsewhere in the content or source repository.

The platforms on this page are catalog indications. The paths below follow tool documentation; they do not prove this skill has been run in those tools. No minimum tool version is confirmed for this listing.

3. Load the skill

After inspection, copy the complete folder into your project using one of the commands below, then open a new tool session. The command stops if a folder with the same name already exists: compare the files before replacing it.

Claude Code
test -f '.skill-downloads/aegis-multi-tenancy-enforcer-mmio70nb/SKILL.md' && test -s '.skill-downloads/aegis-multi-tenancy-enforcer-mmio70nb/SKILL.md' && mkdir -p '.claude/skills' && test ! -e '.claude/skills/tenancy-enforcer' && cp -R '.skill-downloads/aegis-multi-tenancy-enforcer-mmio70nb' '.claude/skills/tenancy-enforcer'

Cursor
test -f '.skill-downloads/aegis-multi-tenancy-enforcer-mmio70nb/SKILL.md' && test -s '.skill-downloads/aegis-multi-tenancy-enforcer-mmio70nb/SKILL.md' && mkdir -p '.cursor/skills' && test ! -e '.cursor/skills/tenancy-enforcer' && cp -R '.skill-downloads/aegis-multi-tenancy-enforcer-mmio70nb' '.cursor/skills/tenancy-enforcer'

OpenSkills

OpenSkills must already be installed. The local install/read procedure is checked with OpenSkills 1.5.0, which requires Node.js ≥ 20.6.0. Catalog slugs are not resolved automatically.

Official OpenSkills installation
openskills install './.skill-downloads/aegis-multi-tenancy-enforcer-mmio70nb'

openskills read 'aegis-multi-tenancy-enforcer-mmio70nb'

Reading should display the installed file content and its base directory. It does not run the skill or install its dependencies.

For an agent that reads AGENTS.md, then sync the index. This command changes AGENTS.md: review the diff before using it.

openskills sync

Manual method or another tool: attach SKILL.md to a conversation, or open it in an agent that can read files. Include any required supporting resources. No universal folder is recognized by every tool.

4. Try a first task

Use a short, synthetic input: an API endpoint, a small document or an interface brief. In a new conversation, attach the file or give the agent its path, then adapt this request:

Read .skill-downloads/aegis-multi-tenancy-enforcer-mmio70nb/SKILL.md and apply its instructions to this example: [describe a test input]. Identify any missing files, dependencies or access. Produce the result requested by the skill without inventing data or running unreviewed scripts.

Expected result: output matching the skill instructions, or a precise list of missing prerequisites. A read confirmation alone does not validate a first use. If an external resource is unavailable, that path remains untested.

Related skills