Skip to content

Vendor Encapsulation Policy

Quick Reference

  • When to use: Every third-party library interaction
  • Enforcement: ZERO TOLERANCE
  • Impact if violated: CRITICAL - Breaks architecture, testability, and observability

The Rule

NEVER import third-party libraries directly in Lambda handlers or business logic. Always use @mantleframework/* package wrappers.

This applies to:

  • AWS SDK (@aws-sdk/*) - Use @mantleframework/aws
  • Drizzle ORM (drizzle-orm) - Use @mantleframework/database
  • Better Auth (better-auth) - Use @mantleframework/auth
  • AWS Lambda Powertools - Use @mantleframework/observability
  • Any other third-party service integration

Wrapper Package Locations

LibraryWrapper PackageWhat It Provides
AWS SDK@mantleframework/awsS3, DynamoDB, SNS, SQS, Lambda clients with X-Ray tracing
Drizzle ORM@mantleframework/databaseDatabase client, query metrics, @RequiresTable decorator
Drizzle operators@mantleframework/database/ormRe-exports of eq, and, sql, type utilities
Better Auth@mantleframework/authConfigured auth instance, session management
Powertools@mantleframework/observabilityLogger, tracer, metrics with consistent configuration
Middy@mantleframework/securityMiddleware chains with security defaults

Examples

AWS SDK

FORBIDDEN

typescript
// Direct SDK imports in handlers
import {S3Client, PutObjectCommand} from '@aws-sdk/client-s3'
import {DynamoDBClient} from '@aws-sdk/client-dynamodb'

const s3 = new S3Client({})

REQUIRED

typescript
// Vendor wrapper imports
import {getS3Client, getDynamoDBClient} from '@mantleframework/aws'

Drizzle ORM

FORBIDDEN

typescript
// Direct Drizzle imports in Lambda handlers
import {drizzle} from 'drizzle-orm/postgres-js'
import {eq} from 'drizzle-orm'
import postgres from 'postgres'

REQUIRED

typescript
// Framework API from @mantleframework/database
import {getDrizzleClient, withQueryMetrics, RequiresTable} from '@mantleframework/database'

// Drizzle operators and types from the /orm subpath
import {eq, and, sql} from '@mantleframework/database/orm'
import type {InferSelectModel, InsertModel} from '@mantleframework/database/orm'

// Schema definitions (only place drizzle-orm/pg-core is allowed)
import {pgTable, text, uuid, timestamp} from 'drizzle-orm/pg-core'

Better Auth

FORBIDDEN

typescript
// Direct Better Auth usage
import {betterAuth} from 'better-auth'
const auth = betterAuth({...})

REQUIRED

typescript
// Use configured auth instance
import {auth} from '@mantleframework/auth'
const session = await auth.api.getSession({...})

Observability

FORBIDDEN

typescript
// Direct Powertools imports
import {Logger} from '@aws-lambda-powertools/logger'
import {Tracer} from '@aws-lambda-powertools/tracer'

REQUIRED

typescript
// Wrapped observability
import {logger, tracer, metrics} from '@mantleframework/observability'

Handler Usage

Handlers use define*Handler functions which provide observability automatically:

typescript
import { buildValidatedResponse } from '@mantleframework/core'
import { defineApiHandler } from '@mantleframework/validation'
import { z } from '@mantleframework/validation'
import { getDrizzleClient } from '@mantleframework/database'
import { putObject } from '@mantleframework/aws'

const ResponseSchema = z.object({ id: z.string() })

const api = defineApiHandler({ auth: 'bearer', operationName: 'CreateItem' })
export const handler = api(async ({ event, context, body }) => {
  const db = getDrizzleClient()
  // ... use db and putObject via @mantle wrappers
  return buildValidatedResponse(context, 201, { id: 'abc' }, ResponseSchema)
})

The Drizzle Import Convention

Drizzle access has a specific three-tier import convention:

@mantleframework/database        - Framework API (getDrizzleClient, withQueryMetrics, RequiresTable)
@mantleframework/database/orm    - Drizzle re-exports (operators: eq, and, sql; types: SelectModel)
drizzle-orm/pg-core     - Schema definitions ONLY (pgTable, column types)

This means:

  • Handler files import from @mantleframework/database and @mantleframework/database/orm
  • Entity query files import from @mantleframework/database and @mantleframework/database/orm
  • Schema files are the ONLY place that imports from drizzle-orm/pg-core

Wrapper Pattern

Each @mantleframework/* package follows this pattern:

typescript
// packages/aws/src/clients/s3.ts
import {S3Client} from '@aws-sdk/client-s3'
import {getRequiredEnv} from '@mantleframework/env'

let client: S3Client | null = null

/** Lazy-initialized S3 client with X-Ray tracing */
export function getS3Client(): S3Client {
  if (!client) {
    client = new S3Client({region: getRequiredEnv('AWS_REGION')})
  }
  return client
}

Key aspects:

  1. Lazy initialization - Client created on first use, not at import time
  2. Environment detection - Uses @mantleframework/env for configuration
  3. Singleton pattern - One client instance per Lambda execution environment
  4. Observability - X-Ray tracing and metrics built in

Rationale

  1. Testability - Mock @mantleframework/aws instead of @aws-sdk/client-s3; one mock point per service
  2. Observability - X-Ray tracing, CloudWatch metrics, and structured logging applied consistently
  3. Configuration - Region, endpoints, and credentials managed in one place
  4. Migration safety - Swap underlying library without changing consumer code
  5. Consistency - Same patterns across all Lambda functions

Enforcement

MethodScope
mantle check CLIValidates all convention rules including vendor encapsulation
MCP validation serverReal-time convention checking with 21+ rules
ESLint local-rules/enforce-powertoolsPowertools imports
ESLint local-rules/env-validationEnvironment variable access
ESLint no-raw-aws-sdk ruleCatches direct @aws-sdk/* imports in handler code
Code ReviewAll vendor libraries
mantle generate permissionsValidates entity query patterns

Testing

typescript
// Mock the @mantle wrapper, not the underlying SDK
vi.mock('@mantleframework/aws', () => ({
  getS3Client: vi.fn(),
  getDynamoDBClient: vi.fn()
}))

// For database tests, mock @mantleframework/database
vi.mock('@mantleframework/database', () => ({
  getDrizzleClient: vi.fn(),
  withQueryMetrics: vi.fn((fn) => fn)
}))

Adding New Vendors

  1. Create wrapper in packages/<vendor-name>/
  2. Follow the package structure convention (build.config.ts, src/index.ts, etc.)
  3. Implement lazy initialization pattern
  4. Add observability (tracing, metrics) where appropriate
  5. Export domain-specific functions
  6. Update this documentation

ZERO TOLERANCE: Always use @mantleframework/* wrappers for third-party libraries. Direct imports of AWS SDK, Drizzle, Better Auth, or Powertools in handler code are never acceptable.