@rpallas/outcrop
v1.1.0
Published
Lambda-first AWS CDK constructs with preview-stack support: PlatformApp, PlatformStack, naming, SSM contract, alerting and opinionated primitives.
Maintainers
Readme
@rpallas/outcrop
Lambda-first AWS CDK constructs with preview-stack support.
npm install @rpallas/outcrop aws-cdk-lib constructsUsage
// platform.config.ts
import { definePlatformConfig } from "@rpallas/outcrop";
export default definePlatformConfig({
project: "Example Platform",
service: "orders",
environments: {
dev: { account: "111111111111", region: "eu-west-1", domain: "dev.example.com" },
prod: { account: "222222222222", region: "eu-west-1", domain: "example.com", protected: true },
},
github: { owner: "example-org", repo: "orders" },
});// infra/bin/app.ts
import { PlatformApp } from "@rpallas/outcrop";
import config from "../../platform.config";
import { ServiceStack } from "../lib/service-stack";
const app = new PlatformApp({ config });
new ServiceStack(app, "Service");// infra/lib/service-stack.ts
import { HttpMethod } from "aws-cdk-lib/aws-apigatewayv2";
import { AttributeType } from "aws-cdk-lib/aws-dynamodb";
import {
lambdaEntry,
PlatformFunction,
PlatformHttpApi,
PlatformStack,
PlatformTable,
} from "@rpallas/outcrop";
export class ServiceStack extends PlatformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
const table = new PlatformTable(this, "Orders", {
partitionKey: { name: "pk", type: AttributeType.STRING },
});
const handler = new PlatformFunction(this, "Handler", {
entry: lambdaEntry("app/src/handlers/orders"),
environment: { TABLE_NAME: table.tableName },
});
table.grantReadWriteData(handler);
const api = new PlatformHttpApi(this, "Api", { cors: true });
api.addLambdaRoute("/orders", [HttpMethod.GET, HttpMethod.POST], handler);
handler.addStandardAlarms();
}
}npx cdk deploy -c env=dev
npx cdk deploy -c env=dev -c preview=true -c previewId=abc-123What you get
PlatformApp/PlatformStack: context resolution, tags, removal policy, log retention, SSM parameter access, alert routingPlatformNaming: deterministic physical names with per-resource limits and preview prefixesPlatformParameters: typed reader for the/platform/...SSM contract written by@rpallas/outcrop-accountPlatformAlarm,PlatformDashboard,serviceDashboard,alertTopic- The constructs below
Every construct derives physical names through PlatformNaming, applies the stack removal policy and tags, uses short log retention / DESTROY / no deletion protection in preview stacks, and exposes an alarms object whose helpers return PlatformAlarms routed to the environment alert topics (silent in previews unless the stack opts in).
Constructs
| Construct | Wraps | Highlights |
| -------------------------------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------- |
| PlatformFunction | NodejsFunction | ARM64, X-Ray, explicit log group, platform env vars, optional DLQ, addStandardAlarms() |
| PlatformHttpApi | HttpApi | Access logs, throttling, CORS, custom domain + Route 53, addLambdaRoute, PlatformHttpAuthorizers |
| PlatformRestApi | RestApi | JSON access logs, X-Ray, env stage, custom domain, validators, API keys, usage plans, WAF |
| PlatformWebSocketApi | WebSocketApi | $connect/$disconnect/$default + custom routes, env stage, access logs, optional domain |
| PlatformTable | TableV2 | On-demand, PITR outside previews, addGsi, throttle / system error alarms |
| PlatformQueue | Queue | SSE, TLS, optional DLQ, age / depth / DLQ alarms |
| PlatformBucket | Bucket | Private, TLS, encryption, lifecycle helpers, notification helpers |
| PlatformTopic | Topic | KMS (account key), TLS policy, Lambda / SQS subscription helpers |
| PlatformEventBus | EventBus | Named bus, optional archive, grantPutEvents, fromPlatform() for the environment bus |
| PlatformEventRule | Rule | eventNames shorthand, automatic target wiring, DLQ, retries |
| PlatformSchedule | Scheduler Schedule | cron()/rate() strings, Lambda / SQS / Step Functions targets, disabled in previews by default |
| PlatformStateMachine | StateMachine | Tracing, log group with platform retention, ALL logs in previews |
| PlatformDistribution | Distribution | Custom domain (us-east-1 cert, A/AAAA aliases), TLS 1.2 2021, HTTP/2+3, access logs |
| PlatformStaticSite | bucket + distribution + deployment | OAC, SPA fallback, security headers, optional API origin |
| PlatformUserPool | UserPool | Strong password policy, optional MFA, hosted UI client helper, lazy default client |
| PlatformKey | Key | Rotation, alias from naming, organisation / principal grants |
| PlatformSecret | Secret | Generated or explicit value, ARN published to the SSM contract, fromPlatform() |
| PlatformParameter | StringParameter | Writes under /platform/services/{service}/..., automatic tier, serviceValue() |
| PlatformWebAcl | CfnWebACL | AWS managed rule groups, IP rate limit, REST API association, forDistribution() |
| PlatformEmailIdentity | SES EmailIdentity | Easy DKIM records in the platform zone, MAIL FROM, configuration set, scoped grantSend |
| PlatformCustomResource | CustomResource | Shared provider per handler, typed properties |
Examples
// Events and schedules
const bus = new PlatformEventBus(this, "Domain", { archive: { retention: Duration.days(30) } });
bus.grantPutEvents(handler);
new PlatformEventRule(this, "OnOrderEvents", {
eventNames: ["order.created", "order.updated"], // DetailType as published by the runtime package
targets: [worker],
deadLetterQueue: true,
}).alarms.failedInvocations();
new PlatformSchedule(this, "Nightly", { schedule: "cron(0 2 * * ? *)", target: worker });
// Secrets and parameters
const apiKey = new PlatformSecret(this, "ApiKey", { name: "api-key", generate: { length: 40 } });
apiKey.grantRead(handler);
new PlatformParameter(this, "ApiUrl", { key: "api-url", value: api.baseUrl });
const billingUrl = PlatformParameter.serviceValue(this, "billing", "api-url");
// Web
const site = new PlatformStaticSite(this, "Site", {
sourcePath: "web/dist",
spa: true,
apiOrigin: { api, pathPattern: "/api/*" },
});
const users = new PlatformUserPool(this, "Users", { selfSignUp: false });
users.addHostedUiClient({ callbackUrls: [`${site.url}/callback`] });
api.addLambdaRoute("/me", HttpMethod.GET, handler, {
authorizer: PlatformHttpAuthorizers.cognito(users),
});
// REST API behind WAF
const firewall = new PlatformWebAcl(this, "Firewall", { rateLimit: 1000 });
const admin = new PlatformRestApi(this, "Admin", { webAcl: firewall });
admin.addLambdaRoute("/admin/orders/{id}", [HttpMethod.GET, HttpMethod.DELETE], handler, {
authorizationType: AuthorizationType.IAM,
requestValidator: admin.addValidator("default"),
});
// Email
const mail = new PlatformEmailIdentity(this, "Mail", { mailFrom: true });
mail.grantSend(handler, ["[email protected]"]);
mail.alarms.bounceRate();
// One dashboard for everything above
serviceDashboard(this);See the repository docs/ for conventions, ADRs and one page per construct under docs/constructs/.
