- Architecture Overview
- Package Structure
- Core Event System
- HTTP Routing System
- Tenant Context System
- AWS Infrastructure Adapters
- Feature Registration
- Lambda Handler Setup
- Migration Strategy
- Testing Strategy
- Cloud Agnostic Core - HTTP routing works on any cloud platform
- Event Type Detection - Lightweight detection before dependency resolution
- Middleware Chain - Handlers can call
next()for request processing - Decorator Pattern - Cross-cutting concerns wrap core functionality
- Tenant Isolation - Per-request container with tenant context
- Type Safety - Full TypeScript support throughout
┌─────────────────────────────────────────────────┐
│ Lambda Entry Point (Per-Request Container) │
└──────────────────┬──────────────────────────────┘
│
┌──────────────────▼──────────────────────────────┐
│ Event Dispatcher (Matches & Chains Handlers) │
└──────────────────┬──────────────────────────────┘
│
┌──────────────┴─────────────┐
│ │
┌───▼──────────────┐ ┌──────────▼───────────────┐
│ HTTP System │ │ Cloud-Specific Events │
│ (Cloud Agnostic)│ │ (S3, SQS, SNS, etc.) │
└──────────────────┘ └──────────────────────────┘
packages/
├── core/
│ └── features/
│ ├── events/ # Generic event system
│ │ ├── abstractions.ts
│ │ ├── EventDispatcher.ts
│ │ └── feature.ts
│ ├── http/ # Cloud-agnostic HTTP
│ │ ├── abstractions.ts
│ │ ├── HttpRouter.ts
│ │ ├── decorators/
│ │ │ ├── TenantInitializerDecorator.ts
│ │ │ └── SecureHeadersDecorator.ts
│ │ ├── routes/
│ │ │ ├── GraphQLRoute.ts
│ │ │ └── FilesRoute.ts
│ │ └── feature.ts
│ └── tenancy/ # Multi-tenancy
│ ├── abstractions.ts
│ ├── TenantContext.ts
│ ├── LoadTenant/
│ │ └── LoadTenantUseCase.ts
│ ├── extractors/
│ │ └── HttpTenantIdExtractor.ts
│ └── feature.ts
└── aws/
└── features/
├── ApiGateway/ # AWS HTTP adapter
│ ├── ApiGatewayEventType.ts
│ ├── ApiGatewayEventHandler.ts
│ └── abstractions.ts
├── S3/ # AWS S3 events
│ ├── S3EventType.ts
│ ├── S3EventHandler.ts
│ ├── S3TenantIdExtractor.ts
│ └── abstractions.ts
├── SQS/ # AWS SQS events
└── feature.ts
File: packages/core/features/events/abstractions.ts
import { createAbstraction } from "@webiny/feature/api";
// ============================================================================
// Event Context (Flows through middleware chain)
// ============================================================================
export interface EventContext<TEvent = any> {
event: TEvent;
metadata: Record<string, any>;
}
export type NextFunction<TEvent = any, TResult = any> = (
context?: EventContext<TEvent>
) => Promise<TResult>;
// ============================================================================
// Event Handler (with middleware chain support)
// ============================================================================
export interface IEventHandler<TEvent = any, TResult = any> {
handle(
context: EventContext<TEvent>,
next: NextFunction<TEvent, TResult>
): Promise<TResult>;
}
export const EventHandler = createAbstraction<IEventHandler>("EventHandler");
export namespace EventHandler {
export type Interface = IEventHandler;
}
// ============================================================================
// Event Type (Lightweight detection + handler reference)
// ============================================================================
export interface IEventType<TEvent = any> {
canHandle(event: any): event is TEvent;
getHandlerAbstraction(): Abstraction<IEventHandler<TEvent, any>>;
}
export const EventType = createAbstraction<IEventType>("EventType");
export namespace EventType {
export type Interface = IEventType;
}
// ============================================================================
// Event Dispatcher (Orchestrates event routing)
// ============================================================================
export interface IEventDispatcher {
dispatch<TEvent, TResult>(event: TEvent): Promise<TResult>;
}
export const EventDispatcher = createAbstraction<IEventDispatcher>("EventDispatcher");
export namespace EventDispatcher {
export type Interface = IEventDispatcher;
}File: packages/core/features/events/EventDispatcher.ts
import { EventDispatcher as Abstraction, EventType, EventContext } from "./abstractions";
import { NoHandlerFoundError } from "./errors";
import type { Container } from "@webiny/di";
class EventDispatcherImpl implements Abstraction.Interface {
constructor(
private eventTypes: EventType.Interface[],
private container: Container // Only dispatcher has container access
) {}
async dispatch<TEvent, TResult>(event: TEvent): Promise<TResult> {
// Step 1: Find matching event type (cheap, no DI resolution)
const eventType = this.eventTypes.find(type => type.canHandle(event));
if (!eventType) {
throw new NoHandlerFoundError("No event type matched the incoming event");
}
// Step 2: Get the handler abstraction for this event type
const handlerAbstraction = eventType.getHandlerAbstraction();
// Step 3: Resolve ALL handlers for this type (for middleware chain)
const handlers = this.container.resolveAll(handlerAbstraction);
if (handlers.length === 0) {
throw new NoHandlerFoundError(
`No handlers registered for ${handlerAbstraction.toString()}`
);
}
// Step 4: Create initial context
const context: EventContext<TEvent> = {
event,
metadata: {}
};
// Step 5: Build and execute middleware chain
return this.executeChain(context, handlers);
}
private async executeChain(
initialContext: EventContext<any>,
handlers: IEventHandler[]
): Promise<any> {
// Last registered handler runs first
let index = handlers.length - 1;
const next = async (context?: EventContext<any>): Promise<any> => {
if (index < 0) {
throw new Error("No more handlers in chain");
}
const handler = handlers[index];
index--;
// Use provided context or initial context
const currentContext = context || initialContext;
return handler.handle(currentContext, next);
};
return next(initialContext);
}
}
export const EventDispatcherImpl = Abstraction.createImplementation({
implementation: EventDispatcherImpl,
dependencies: [
[EventType, { multiple: true }],
Container // Special: dispatcher needs container for lazy resolution
]
});File: packages/core/features/events/feature.ts
import { createFeature } from "@webiny/feature";
import { EventDispatcherImpl } from "./EventDispatcher";
export const EventsFeature = createFeature({
name: "Events",
register(container) {
container.register(EventDispatcherImpl).inSingletonScope();
}
});File: packages/core/features/events/errors.ts
import { BaseError } from "@webiny/feature/api";
export class NoHandlerFoundError extends BaseError {
override readonly code = "NO_HANDLER_FOUND" as const;
constructor(message?: string) {
super({
message: message || "No handler found for event",
data: {}
});
}
}File: packages/core/features/http/abstractions.ts
import { createAbstraction } from "@webiny/feature/api";
// ============================================================================
// HTTP Request/Response (Cloud Agnostic)
// ============================================================================
export interface IHttpRequest {
method: string;
path: string;
headers: Record<string, string>;
query: Record<string, string>;
body: any;
pathParameters: Record<string, string>;
}
export interface IHttpResponse {
statusCode: number;
headers: Record<string, string>;
body: any;
}
// ============================================================================
// HTTP Route (Combines route definition + handler)
// ============================================================================
export interface IHttpRoute {
readonly method: string;
readonly path: string;
handle(request: IHttpRequest): Promise<IHttpResponse>;
}
export const HttpRoute = createAbstraction<IHttpRoute>("HttpRoute");
export namespace HttpRoute {
export type Interface = IHttpRoute;
}
// ============================================================================
// HTTP Router (Matches routes and executes handlers)
// ============================================================================
export interface IHttpRouter {
route(request: IHttpRequest): Promise<IHttpResponse>;
}
export const HttpRouter = createAbstraction<IHttpRouter>("HttpRouter");
export namespace HttpRouter {
export type Interface = IHttpRouter;
}File: packages/core/features/http/HttpRouter.ts
import { HttpRouter as Abstraction, HttpRoute } from "./abstractions";
import { RouteNotFoundError } from "./errors";
class HttpRouterImpl implements Abstraction.Interface {
constructor(private routes: HttpRoute.Interface[]) {}
async route(request: IHttpRequest): Promise<IHttpResponse> {
// Find matching route
for (const route of this.routes) {
if (this.matches(route, request)) {
return route.handle(request);
}
}
throw new RouteNotFoundError(request.method, request.path);
}
private matches(route: HttpRoute.Interface, request: IHttpRequest): boolean {
// Method must match
if (route.method !== request.method) {
return false;
}
// Wildcard path matching
if (route.path.endsWith("/*")) {
const prefix = route.path.slice(0, -2);
return request.path.startsWith(prefix);
}
// Exact path matching
return route.path === request.path;
}
}
export const HttpRouterImpl = Abstraction.createImplementation({
implementation: HttpRouterImpl,
dependencies: [[HttpRoute, { multiple: true }]]
});File: packages/core/features/http/routes/GraphQLRoute.ts
import { HttpRoute } from "../abstractions";
import { GraphQLEngine } from "../../graphql/abstractions";
class GraphQLRoute implements HttpRoute.Interface {
readonly method = "POST";
readonly path = "/graphql";
constructor(private graphqlEngine: GraphQLEngine.Interface) {}
async handle(request: IHttpRequest): Promise<IHttpResponse> {
const result = await this.graphqlEngine.execute(request.body);
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: result
};
}
}
export const GraphQLRouteImpl = HttpRoute.createImplementation({
implementation: GraphQLRoute,
dependencies: [GraphQLEngine]
});File: packages/core/features/http/routes/FilesRoute.ts
import { HttpRoute } from "../abstractions";
import { FileStorage } from "../../storage/abstractions";
class FilesRoute implements HttpRoute.Interface {
readonly method = "GET";
readonly path = "/files/*";
constructor(private storage: FileStorage.Interface) {}
async handle(request: IHttpRequest): Promise<IHttpResponse> {
const key = request.path.replace("/files/", "");
const file = await this.storage.get(key);
return {
statusCode: 200,
headers: {
"Content-Type": file.contentType,
"Cache-Control": "public, max-age=31536000"
},
body: file.content
};
}
}
export const FilesRouteImpl = HttpRoute.createImplementation({
implementation: FilesRoute,
dependencies: [FileStorage]
});File: packages/core/features/http/decorators/SecureHeadersDecorator.ts
import { HttpRouter } from "../abstractions";
class SecureHeadersDecorator implements HttpRouter.Interface {
private readonly whitelistedHeaders = [
"accept",
"authorization",
"cache-control",
"content-type",
"x-i18n-locale",
"x-tenant",
"x-apollo-tracing",
"apollo-query-plan-experimental"
];
constructor(private decoratee: HttpRouter.Interface) {}
async route(request: IHttpRequest): Promise<IHttpResponse> {
const isOptions = request.method === "OPTIONS";
// Handle OPTIONS early (no need to route)
if (isOptions) {
return this.createOptionsResponse(request);
}
// Route to actual handler
const response = await this.decoratee.route(request);
// Add security headers to response
return this.addSecurityHeaders(request, response);
}
private createOptionsResponse(request: IHttpRequest): IHttpResponse {
return {
statusCode: 204,
headers: {
"access-control-allow-origin": request.headers["origin"] || "*",
"access-control-allow-credentials": "true",
"cache-control": "public, max-age=86400",
"content-type": "application/json; charset=utf-8",
"access-control-max-age": "86400",
"access-control-allow-methods": "OPTIONS,POST,GET,DELETE,PUT,PATCH",
"access-control-allow-headers": this.whitelistedHeaders.join(", ")
},
body: null
};
}
private addSecurityHeaders(
request: IHttpRequest,
response: IHttpResponse
): IHttpResponse {
return {
...response,
headers: {
...response.headers,
"access-control-allow-origin": request.headers["origin"] || "*",
"access-control-allow-credentials": "true",
"x-tenant": request.headers["x-tenant"] || "root",
"vary": "origin"
}
};
}
}
export const SecureHeadersDecoratorImpl = HttpRouter.createDecorator({
decorator: SecureHeadersDecorator,
dependencies: []
});File: packages/core/features/http/feature.ts
import { createFeature } from "@webiny/feature";
import { HttpRouterImpl } from "./HttpRouter";
import { SecureHeadersDecoratorImpl } from "./decorators/SecureHeadersDecorator";
import { GraphQLRouteImpl } from "./routes/GraphQLRoute";
import { FilesRouteImpl } from "./routes/FilesRoute";
export const HttpFeature = createFeature({
name: "Http",
register(container) {
// Register router
container.register(HttpRouterImpl).inSingletonScope();
// Register decorators (order: last registered runs first)
container.registerDecorator(SecureHeadersDecoratorImpl);
// Register routes
container.register(GraphQLRouteImpl);
container.register(FilesRouteImpl);
}
});File: packages/core/features/http/errors.ts
import { BaseError } from "@webiny/feature/api";
export class RouteNotFoundError extends BaseError<{ method: string; path: string }> {
override readonly code = "ROUTE_NOT_FOUND" as const;
constructor(method: string, path: string) {
super({
message: `Route not found: ${method} ${path}`,
data: { method, path }
});
}
}
export class HttpHandlerError extends BaseError {
override readonly code = "HTTP_HANDLER_ERROR" as const;
constructor(message: string) {
super({
message,
data: {}
});
}
}File: packages/core/features/tenancy/abstractions.ts
import { createAbstraction } from "@webiny/feature/api";
import { Result } from "@webiny/feature/api";
// ============================================================================
// Tenant Entity (Domain)
// ============================================================================
export interface Tenant {
id: string;
name: string;
status: "active" | "suspended" | "deleted";
settings: Record<string, any>;
createdAt: Date;
updatedAt: Date;
}
// ============================================================================
// Tenant Context (Request-scoped singleton)
// ============================================================================
export interface ITenantContext {
get(): Tenant | undefined;
set(tenant: Tenant): void;
requireTenant(): Tenant; // Throws if not set
}
export const TenantContext = createAbstraction<ITenantContext>("TenantContext");
export namespace TenantContext {
export type Interface = ITenantContext;
}
// ============================================================================
// Load Tenant Use Case
// ============================================================================
export interface ILoadTenant {
execute(tenantId: string): Promise<Result<Tenant, LoadTenant.Error>>;
}
export const LoadTenant = createAbstraction<ILoadTenant>("LoadTenant");
export namespace LoadTenant {
export type Interface = ILoadTenant;
export type Error = TenantNotFoundError | TenantLoadError;
}File: packages/core/features/tenancy/TenantContext.ts
import { TenantContext as Abstraction } from "./abstractions";
import { TenantNotSetError } from "./errors";
class TenantContextImpl implements Abstraction.Interface {
private tenant?: Tenant;
get(): Tenant | undefined {
return this.tenant;
}
set(tenant: Tenant): void {
if (this.tenant) {
throw new Error("Tenant already set in this context");
}
this.tenant = tenant;
}
requireTenant(): Tenant {
if (!this.tenant) {
throw new TenantNotSetError();
}
return this.tenant;
}
}
export const TenantContextImpl = Abstraction.createImplementation({
implementation: TenantContextImpl,
dependencies: []
});File: packages/core/features/tenancy/LoadTenant/LoadTenantUseCase.ts
import { LoadTenant as Abstraction } from "../abstractions";
import { TenantsRepository } from "../shared/abstractions";
import { TenantNotFoundError } from "../errors";
import { Result } from "@webiny/feature/api";
class LoadTenantUseCase implements Abstraction.Interface {
constructor(private repository: TenantsRepository.Interface) {}
async execute(tenantId: string): Promise<Result<Tenant, Abstraction.Error>> {
const result = await this.repository.get(tenantId);
if (result.isFail()) {
return Result.fail(result.error);
}
if (!result.value) {
return Result.fail(new TenantNotFoundError(tenantId));
}
// Validate tenant is active
if (result.value.status !== "active") {
return Result.fail(new TenantNotActiveError(tenantId));
}
return Result.ok(result.value);
}
}
export const LoadTenantUseCaseImpl = Abstraction.createImplementation({
implementation: LoadTenantUseCase,
dependencies: [TenantsRepository]
});File: packages/core/features/tenancy/extractors/HttpTenantIdExtractor.ts
export class HttpTenantIdExtractor {
extract(request: IHttpRequest): string | undefined {
// Option 1: From header
const headerTenant = request.headers['x-tenant'] || request.headers['X-Tenant'];
if (headerTenant) {
return headerTenant;
}
// Option 2: From subdomain
const host = request.headers.Host || request.headers.host || "";
const parts = host.split(".");
// Example: tenant123.api.example.com -> tenant123
if (parts.length >= 3) {
const subdomain = parts[0];
if (subdomain && subdomain !== "www" && subdomain !== "api") {
return subdomain;
}
}
return undefined;
}
}File: packages/core/features/http/decorators/TenantInitializerDecorator.ts
import { HttpRouter } from "../abstractions";
import { TenantContext, LoadTenant } from "../../tenancy/abstractions";
import { HttpTenantIdExtractor } from "../../tenancy/extractors/HttpTenantIdExtractor";
class TenantInitializerDecorator implements HttpRouter.Interface {
constructor(
private tenantContext: TenantContext.Interface,
private loadTenant: LoadTenant.Interface,
private extractor: HttpTenantIdExtractor,
private decoratee: HttpRouter.Interface
) {}
async route(request: IHttpRequest): Promise<IHttpResponse> {
// Extract tenant ID from request
const tenantId = this.extractor.extract(request);
if (!tenantId) {
return {
statusCode: 400,
headers: {},
body: { error: "Tenant ID not found in request" }
};
}
// Load tenant from database
const result = await this.loadTenant.execute(tenantId);
if (result.isFail()) {
return {
statusCode: 404,
headers: {},
body: { error: `Tenant not found: ${tenantId}` }
};
}
// Set tenant in context for all downstream use cases
this.tenantContext.set(result.value);
// Route to handler (tenant is now available)
return this.decoratee.route(request);
}
}
export const TenantInitializerDecoratorImpl = HttpRouter.createDecorator({
decorator: TenantInitializerDecorator,
dependencies: [TenantContext, LoadTenant, HttpTenantIdExtractor]
});File: packages/core/features/http/feature.ts
import { createFeature } from "@webiny/feature";
import { HttpRouterImpl } from "./HttpRouter";
import { SecureHeadersDecoratorImpl } from "./decorators/SecureHeadersDecorator";
import { TenantInitializerDecoratorImpl } from "./decorators/TenantInitializerDecorator";
import { GraphQLRouteImpl } from "./routes/GraphQLRoute";
import { FilesRouteImpl } from "./routes/FilesRoute";
export const HttpFeature = createFeature({
name: "Http",
register(container) {
// Register router
container.register(HttpRouterImpl).inSingletonScope();
// Register decorators (order: last registered runs first)
// Execution: SecureHeaders → TenantInitializer → HttpRouter
container.registerDecorator(SecureHeadersDecoratorImpl);
container.registerDecorator(TenantInitializerDecoratorImpl);
// Register routes
container.register(GraphQLRouteImpl);
container.register(FilesRouteImpl);
}
});File: packages/core/features/tenancy/feature.ts
import { createFeature } from "@webiny/feature";
import { TenantContextImpl } from "./TenantContext";
import { LoadTenantUseCaseImpl } from "./LoadTenant/LoadTenantUseCase";
import { TenantsRepositoryImpl } from "./shared/TenantsRepository";
import { HttpTenantIdExtractor } from "./extractors/HttpTenantIdExtractor";
export const TenancyFeature = createFeature({
name: "Tenancy",
register(container) {
// Register tenant context as singleton (per-request container)
container.register(TenantContextImpl).inSingletonScope();
// Register use case
container.register(LoadTenantUseCaseImpl);
// Register repository
container.register(TenantsRepositoryImpl).inSingletonScope();
// Register extractor
container.registerInstance(HttpTenantIdExtractor, new HttpTenantIdExtractor());
}
});File: packages/core/features/tenancy/errors.ts
import { BaseError } from "@webiny/feature/api";
export class TenantNotFoundError extends BaseError<{ tenantId: string }> {
override readonly code = "TENANT_NOT_FOUND" as const;
constructor(tenantId: string) {
super({
message: `Tenant not found: ${tenantId}`,
data: { tenantId }
});
}
}
export class TenantNotSetError extends BaseError {
override readonly code = "TENANT_NOT_SET" as const;
constructor() {
super({
message: "Tenant not set in context",
data: {}
});
}
}
export class TenantNotActiveError extends BaseError<{ tenantId: string }> {
override readonly code = "TENANT_NOT_ACTIVE" as const;
constructor(tenantId: string) {
super({
message: `Tenant is not active: ${tenantId}`,
data: { tenantId }
});
}
}
export class TenantLoadError extends BaseError {
override readonly code = "TENANT_LOAD_ERROR" as const;
constructor(error: Error) {
super({
message: `Failed to load tenant: ${error.message}`,
data: {}
});
}
}File: packages/aws/features/ApiGateway/ApiGatewayEventType.ts
import { EventType } from "@webiny/core/features/events";
import { ApiGatewayEventHandler } from "./abstractions";
import type { APIGatewayProxyEvent } from "aws-lambda";
class ApiGatewayEventType implements EventType.Interface<APIGatewayProxyEvent> {
canHandle(event: any): event is APIGatewayProxyEvent {
// Lightweight check - no dependencies resolved
return !!(
event.httpMethod &&
event.path &&
event.requestContext?.requestId
);
}
getHandlerAbstraction() {
return ApiGatewayEventHandler;
}
}
export const ApiGatewayEventTypeImpl = EventType.createImplementation({
implementation: ApiGatewayEventType,
dependencies: []
});File: packages/aws/features/ApiGateway/abstractions.ts
import { createAbstraction } from "@webiny/feature/api";
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import type { IEventHandler, NextFunction } from "@webiny/core/features/events";
export interface IApiGatewayEventHandler
extends IEventHandler<APIGatewayProxyEvent, APIGatewayProxyResult> {
handle(
context: EventContext<APIGatewayProxyEvent>,
next: NextFunction<APIGatewayProxyEvent, APIGatewayProxyResult>
): Promise<APIGatewayProxyResult>;
}
export const ApiGatewayEventHandler = createAbstraction<IApiGatewayEventHandler>(
"ApiGatewayEventHandler"
);
export namespace ApiGatewayEventHandler {
export type Interface = IApiGatewayEventHandler;
}File: packages/aws/features/ApiGateway/handlers/HttpRouterHandler.ts
import { ApiGatewayEventHandler } from "../abstractions";
import { HttpRouter } from "@webiny/core/features/http/abstractions";
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import type { EventContext, NextFunction } from "@webiny/core/features/events";
class HttpRouterHandler implements ApiGatewayEventHandler.Interface {
constructor(private router: HttpRouter.Interface) {}
async handle(
context: EventContext<APIGatewayProxyEvent>,
next: NextFunction<APIGatewayProxyEvent, APIGatewayProxyResult>
): Promise<APIGatewayProxyResult> {
const event = context.event;
// Convert to IHttpRequest
const request: IHttpRequest = {
method: event.httpMethod,
path: event.path,
headers: event.headers || {},
query: event.queryStringParameters || {},
body: event.body ? JSON.parse(event.body) : undefined,
pathParameters: event.pathParameters || {}
};
try {
const response = await this.router.route(request);
return {
statusCode: response.statusCode,
headers: response.headers,
body: JSON.stringify(response.body)
};
} catch (error) {
console.error("HTTP routing error:", error);
return {
statusCode: 500,
headers: {},
body: JSON.stringify({
error: error.message || "Internal server error"
})
};
}
}
}
export const HttpRouterHandlerImpl = ApiGatewayEventHandler.createImplementation({
implementation: HttpRouterHandler,
dependencies: [HttpRouter]
});File: packages/aws/features/ApiGateway/feature.ts
import { createFeature } from "@webiny/feature";
import { ApiGatewayEventTypeImpl } from "./ApiGatewayEventType";
import { HttpRouterHandlerImpl } from "./handlers/HttpRouterHandler";
export const ApiGatewayFeature = createFeature({
name: "ApiGateway",
register(container) {
// Register event type (lightweight detection)
container.register(ApiGatewayEventTypeImpl);
// Register handler
container.register(HttpRouterHandlerImpl);
}
});File: packages/aws/features/S3/S3EventType.ts
import { EventType } from "@webiny/core/features/events";
import { S3EventHandler } from "./abstractions";
import type { S3Event } from "aws-lambda";
class S3EventType implements EventType.Interface<S3Event> {
canHandle(event: any): event is S3Event {
// Lightweight check
return !!(
event.Records &&
event.Records[0]?.eventSource === "aws:s3"
);
}
getHandlerAbstraction() {
return S3EventHandler;
}
}
export const S3EventTypeImpl = EventType.createImplementation({
implementation: S3EventType,
dependencies: []
});File: packages/aws/features/S3/S3EventHandler.ts
import { S3EventHandler as HandlerAbstraction } from "./abstractions";
import { TenantContext, LoadTenant } from "@webiny/core/features/tenancy";
import { S3TenantIdExtractor } from "./S3TenantIdExtractor";
import { ImageProcessor } from "@webiny/core/features/media";
import type { S3Event } from "aws-lambda";
import type { EventContext, NextFunction } from "@webiny/core/features/events";
class S3EventHandler implements HandlerAbstraction.Interface {
constructor(
private tenantContext: TenantContext.Interface,
private loadTenant: LoadTenant.Interface,
private extractor: S3TenantIdExtractor,
private imageProcessor: ImageProcessor.Interface
) {}
async handle(
context: EventContext<S3Event>,
next: NextFunction<S3Event, void>
): Promise<void> {
const event = context.event;
// Extract tenant ID from S3 bucket name
const tenantId = this.extractor.extract(event);
if (!tenantId) {
throw new Error("Cannot determine tenant from S3 event");
}
// Load tenant from database
const result = await this.loadTenant.execute(tenantId);
if (result.isFail()) {
throw new Error(`Tenant not found: ${tenantId}`);
}
// Set tenant in context
this.tenantContext.set(result.value);
// Process S3 event
for (const record of event.Records) {
const bucket = record.s3.bucket.name;
const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, " "));
console.log(`Processing S3 object: ${bucket}/${key} for tenant ${tenantId}`);
// Use case can now access tenant via TenantContext
await this.imageProcessor.process(bucket, key);
}
}
}
export const S3EventHandlerImpl = HandlerAbstraction.createImplementation({
implementation: S3EventHandler,
dependencies: [TenantContext, LoadTenant, S3TenantIdExtractor, ImageProcessor]
});File: packages/aws/features/S3/S3TenantIdExtractor.ts
import type { S3Event } from "aws-lambda";
export class S3TenantIdExtractor {
extract(event: S3Event): string | undefined {
// Extract from bucket name pattern: "tenant123-uploads" -> "tenant123"
const bucket = event.Records[0]?.s3.bucket.name;
if (!bucket) {
return undefined;
}
// Split on first hyphen
const parts = bucket.split("-");
return parts[0];
}
}File: packages/aws/features/S3/abstractions.ts
import { createAbstraction } from "@webiny/feature/api";
import type { S3Event } from "aws-lambda";
import type { IEventHandler } from "@webiny/core/features/events";
export interface IS3EventHandler extends IEventHandler<S3Event, void> {}
export const S3EventHandler = createAbstraction<IS3EventHandler>("S3EventHandler");
export namespace S3EventHandler {
export type Interface = IS3EventHandler;
}File: packages/aws/features/S3/feature.ts
import { createFeature } from "@webiny/feature";
import { S3EventTypeImpl } from "./S3EventType";
import { S3EventHandlerImpl } from "./S3EventHandler";
import { S3TenantIdExtractor } from "./S3TenantIdExtractor";
export const S3Feature = createFeature({
name: "S3",
register(container) {
// Register event type
container.register(S3EventTypeImpl);
// Register extractor
container.registerInstance(S3TenantIdExtractor, new S3TenantIdExtractor());
// Register handler
container.register(S3EventHandlerImpl);
}
});File: packages/aws/features/feature.ts
import { createFeature } from "@webiny/feature";
import { ApiGatewayFeature } from "./ApiGateway/feature";
import { S3Feature } from "./S3/feature";
export const AwsFeature = createFeature({
name: "Aws",
register(container) {
// Register all AWS event features
ApiGatewayFeature.register(container);
S3Feature.register(container);
// Add more AWS features as needed:
// SqsFeature.register(container);
// SnsFeature.register(container);
// DynamoDBFeature.register(container);
}
});File: packages/aws/handler.ts
import { Container } from "@webiny/di";
import { EventsFeature } from "@webiny/core/features/events";
import { TenancyFeature } from "@webiny/core/features/tenancy";
import { HttpFeature } from "@webiny/core/features/http";
import { AwsFeature } from "./features/feature";
import { EventDispatcher } from "@webiny/core/features/events";
// Create base container with all features registered
const baseContainer = new Container();
// Register core features (cloud-agnostic)
EventsFeature.register(baseContainer);
TenancyFeature.register(baseContainer);
HttpFeature.register(baseContainer);
// Register AWS-specific features
AwsFeature.register(baseContainer);
// Lambda handler factory
export const createHandler = () => {
return async (event: any) => {
// Create child container for this request (isolates TenantContext)
const requestContainer = baseContainer.createChildContainer();
// Resolve dispatcher for this request
const dispatcher = requestContainer.resolve(EventDispatcher);
// Dispatch event (will auto-detect type and route)
return dispatcher.dispatch(event);
};
};
// Export the handler
export const handler = createHandler();File: packages/aws/handler-with-app.ts
import { Container } from "@webiny/di";
import { EventsFeature } from "@webiny/core/features/events";
import { TenancyFeature } from "@webiny/core/features/tenancy";
import { HttpFeature } from "@webiny/core/features/http";
import { GraphQLFeature } from "@webiny/core/features/graphql";
import { CmsFeature } from "@webiny/core/features/cms";
import { MediaFeature } from "@webiny/core/features/media";
import { AwsFeature } from "./features/feature";
import { EventDispatcher } from "@webiny/core/features/events";
const baseContainer = new Container();
// Register core infrastructure features
EventsFeature.register(baseContainer);
TenancyFeature.register(baseContainer);
HttpFeature.register(baseContainer);
// Register application features
GraphQLFeature.register(baseContainer);
CmsFeature.register(baseContainer);
MediaFeature.register(baseContainer);
// Register cloud-specific features
AwsFeature.register(baseContainer);
export const handler = async (event: any) => {
const requestContainer = baseContainer.createChildContainer();
const dispatcher = requestContainer.resolve(EventDispatcher);
return dispatcher.dispatch(event);
};Tasks:
- Create package structure (
packages/core,packages/aws) - Implement core event system
EventDispatcherEventTypeEventHandler
- Create base features
- Write unit tests for event dispatcher
Success Criteria:
- Event dispatcher can match events to handlers
- Middleware chain executes in correct order
- Tests pass
Tasks:
- Implement HTTP abstractions
- Create
HttpRouter - Build decorators (SecureHeaders, TenantInitializer)
- Create example routes (GraphQL, Files)
- Write integration tests
Success Criteria:
- Routes can be registered and matched
- Decorators wrap router correctly
- OPTIONS requests handled properly
Tasks:
- Implement
TenantContext - Create
LoadTenantuse case - Build tenant ID extractors
- Integrate with HTTP router
- Test tenant isolation
Success Criteria:
- Tenant loaded from request
- Use cases can access tenant context
- Per-request isolation works
Tasks:
- Create API Gateway adapter
- Create S3 event handler
- Build tenant extractors for each event type
- Test with real AWS events
- Add SQS, SNS handlers as needed
Success Criteria:
- API Gateway events route to HTTP system
- S3 events processed with tenant context
- All AWS event types supported
Tasks:
- Migrate existing routes to new system
- Remove old fastify-based infrastructure
- Update tests
- Performance testing
- Documentation
Success Criteria:
- All routes migrated
- Old code removed
- Performance acceptable
- Documentation complete
Event Dispatcher:
describe("EventDispatcher", () => {
it("should match event to correct handler", async () => {
// Test event type detection
});
it("should execute middleware chain in correct order", async () => {
// Test middleware execution
});
it("should throw when no handler found", async () => {
// Test error handling
});
});HTTP Router:
describe("HttpRouter", () => {
it("should match exact path", async () => {
// Test exact matching
});
it("should match wildcard path", async () => {
// Test wildcard matching
});
it("should throw when route not found", async () => {
// Test 404 handling
});
});Tenant Context:
describe("TenantContext", () => {
it("should set and get tenant", () => {
// Test basic operations
});
it("should throw when tenant not set", () => {
// Test requireTenant()
});
it("should prevent setting tenant twice", () => {
// Test immutability
});
});HTTP Flow:
describe("HTTP Request Flow", () => {
it("should process full request with decorators", async () => {
const container = new Container();
HttpFeature.register(container);
TenancyFeature.register(container);
const dispatcher = container.resolve(EventDispatcher);
const event = createMockApiGatewayEvent({
method: "POST",
path: "/graphql",
headers: { "x-tenant": "test-tenant" }
});
const result = await dispatcher.dispatch(event);
expect(result.statusCode).toBe(200);
});
});Tenant Isolation:
describe("Tenant Isolation", () => {
it("should isolate tenants across requests", async () => {
const baseContainer = new Container();
HttpFeature.register(baseContainer);
// Request 1
const container1 = baseContainer.createChildContainer();
const context1 = container1.resolve(TenantContext);
context1.set({ id: "tenant-1", ... });
// Request 2
const container2 = baseContainer.createChildContainer();
const context2 = container2.resolve(TenantContext);
expect(context2.get()).toBeUndefined();
});
});describe("Performance", () => {
it("should handle 1000 requests/second", async () => {
// Load test
});
it("should resolve dependencies in <10ms", async () => {
// DI performance test
});
});File: packages/core/features/cms/CreatePage/CreatePageUseCase.ts
import { CreatePage } from "./abstractions";
import { TenantContext } from "../../tenancy/abstractions";
import { PagesRepository } from "../shared/abstractions";
import { EventPublisher } from "../../events/abstractions";
import { PageCreatedEvent } from "./events";
import { Result } from "@webiny/feature/api";
class CreatePageUseCase implements CreatePage.Interface {
constructor(
private tenantContext: TenantContext.Interface,
private repository: PagesRepository.Interface,
private eventPublisher: EventPublisher.Interface
) {}
async execute(input: CreatePageInput): Promise<Result<Page, CreatePage.Error>> {
// Get tenant - available because decorator initialized it
const tenant = this.tenantContext.requireTenant();
const page = {
id: generateId(),
tenantId: tenant.id, // Use for data isolation
title: input.title,
content: input.content,
createdAt: new Date(),
updatedAt: new Date()
};
const result = await this.repository.create(page);
if (result.isFail()) {
return Result.fail(result.error);
}
// Publish domain event
await this.eventPublisher.publish(
new PageCreatedEvent({ page: result.value })
);
return Result.ok(result.value);
}
}
export const CreatePageUseCaseImpl = CreatePage.createImplementation({
implementation: CreatePageUseCase,
dependencies: [TenantContext, PagesRepository, EventPublisher]
});- ✅ Cloud Agnostic - HTTP routes work on AWS, Azure, GCP
- ✅ Performance - Event type detection is lightweight
- ✅ Middleware - Chain handlers with
next() - ✅ Tenant Isolation - Per-request containers
- ✅ Type Safe - Full TypeScript support
- ✅ Testable - Easy to mock and test
- ✅ Scalable - Add new event types easily
- ✅ Clean - Clear separation of concerns
- ✅ DI-Based - Everything injected, nothing global
- ✅ Decorator Pattern - Cross-cutting concerns handled cleanly
- Review this plan with the team
- Set up package structure
- Implement Phase 1 (Event System)
- Write tests for each component
- Iterate and refine based on real usage
- Error Handling: How should we handle errors in middleware chain?
- Logging: Should we have a logging decorator?
- Metrics: How to track performance?
- Rate Limiting: Decorator or separate concern?
- Caching: Where does caching fit?
- Validation: Request validation decorator?
This is a living document. Update as the implementation evolves.