# Wraps > Deploy email, SMS, and CDN infrastructure to your AWS account with one command. > Modern DX. AWS economics. Full ownership. Wraps is a CLI and TypeScript SDK that deploys production-ready AWS infrastructure (SES, Lambda, DynamoDB, EventBridge, CloudFront) to your account. Zero credentials stored. OIDC authentication. You own everything. ## What Wraps Does ### CLI (`@wraps.dev/cli`) One command deploys complete infrastructure to your AWS account: ```bash npx @wraps.dev/cli email init # Email (SES + event processing) npx @wraps.dev/cli sms init # SMS (End User Messaging) npx @wraps.dev/cli cdn init # CDN (S3 + CloudFront) ``` **Email deploys:** AWS SES with DKIM, Lambda event processing, DynamoDB history, EventBridge + SQS event capture, IAM roles with OIDC. **SMS deploys:** AWS End User Messaging with phone number provisioning, opt-out list, event tracking, IAM roles. **CDN deploys:** S3 bucket with CloudFront distribution, optional custom domain with ACM certificate. ### TypeScript SDKs ```typescript // @wraps.dev/email import { WrapsEmail } from '@wraps.dev/email'; const email = new WrapsEmail(); await email.send({ from: 'hello@yourapp.com', to: 'user@example.com', subject: 'Welcome!', html: '
Hello Alice!
' }, { to: 'bob@example.com', subject: 'Hi Bob', html: 'Hello Bob!
' }, ], replyTo: 'support@company.com', // Optional, shared across all entries tags: { campaign: 'welcome' }, // Optional default tags (overridable per entry) }); // Returns: { results: BatchEntryResult[], successCount: number, failureCount: number } // Each result: { index, messageId?, status: 'success' | 'failure', error? } // Throws BatchError on partial failure ``` ### email.sendBulkTemplate(params) ```typescript await email.sendBulkTemplate({ from: 'you@company.com', template: 'weekly-digest', destinations: [ // Up to 50 { to: 'alice@example.com', templateData: { name: 'Alice' } }, { to: 'bob@example.com', templateData: { name: 'Bob' } }, ], }); ``` ### email.templates ```typescript await email.templates.create({ name, subject, html, text? }); await email.templates.createFromReact({ name, subject, react }); await email.templates.update({ name, subject?, html?, text? }); await email.templates.get(name); // Returns template details await email.templates.list(); // Returns template metadata[] await email.templates.delete(name); ``` ### Optional Features: Inbox, Events, Suppression Inbox and Events are enabled by constructor config. Suppression is always available. ```typescript const email = new WrapsEmail({ inboxBucketName: 'my-inbox-bucket', // enables email.inbox historyTableName: 'my-events-table', // enables email.events }); // email.inbox is null unless inboxBucketName is provided // email.events is null unless historyTableName is provided // email.suppression is always available ``` ### email.inbox (requires inboxBucketName) ```typescript if (email.inbox) { await email.inbox.list({ maxResults: 20 }); await email.inbox.get(messageId); await email.inbox.getAttachment(messageId, attachmentId); // Returns presigned URL (1hr) await email.inbox.getRaw(messageId); await email.inbox.delete(messageId); await email.inbox.forward(messageId, { to: 'other@example.com', from: 'me@example.com' }); await email.inbox.reply(messageId, { from: 'me@example.com', html: 'Reply
' }); } ``` ### email.events (requires historyTableName) ```typescript if (email.events) { await email.events.get(messageId); // Events sorted by priority await email.events.list({ accountId: '123456789012', maxResults: 50 }); } ``` ### email.suppression (always available) ```typescript await email.suppression.get(emailAddress); await email.suppression.add(emailAddress, 'BOUNCE'); await email.suppression.remove(emailAddress); await email.suppression.list({ reason: 'BOUNCE', maxResults: 100 }); ``` ### Error Types ```typescript import { ValidationError, SESError, DynamoDBError, BatchError, WrapsEmailError } from '@wraps.dev/email'; // WrapsEmailError: base class for all email errors // ValidationError: invalid input (bad email, missing fields) // .field?: string - which field failed // .message: string // SESError: AWS SES error (rate limit, unverified sender) // .code: string - 'MessageRejected', 'Throttling', etc. // .requestId: string // .retryable: boolean // DynamoDBError: email history read/write error // .code: string, .requestId: string, .retryable: boolean // BatchError: partial failure in sendBatch() // .results: BatchEntryResult[] - per-entry results // .successCount: number // .failureCount: number ``` ### SDK Defaults & Limits - No automatic retry (implement your own if retryable is true) - Pagination: 20 items per page default - Presigned URL expiry: 1 hour - Bulk send: 50 destinations max per call - Attachments: 100 max, 10MB total ## SMS SDK Reference (@wraps.dev/sms) ### Constructor ```typescript import { WrapsSMS } from '@wraps.dev/sms'; const sms = new WrapsSMS({ region?: string, credentials?: { accessKeyId, secretAccessKey }, roleArn?: string, }); ``` ### sms.send(params) ```typescript const result = await sms.send({ to: '+14155551234', // Required (E.164) message: 'Hello!', // Required messageType?: 'TRANSACTIONAL', // TRANSACTIONAL or PROMOTIONAL from?: '+18005551234', // Override sender context?: { userId: '123' }, // Custom metadata dryRun?: true, // Validate without sending }); // Returns: { messageId, status, to, from, segments } ``` ### sms.sendBatch(params) ```typescript const result = await sms.sendBatch({ messages: [ { to: '+14155551234', message: 'Hello Alice!' }, { to: '+14155555678', message: 'Hello Bob!' }, ], messageType: 'TRANSACTIONAL', }); // Returns: { batchId, total, queued, failed, results[] } ``` ### sms.numbers.list() ```typescript const numbers = await sms.numbers.list(); // Returns: [{ phoneNumberId, phoneNumber, numberType, messageType, twoWayEnabled }] ``` ### sms.optOuts ```typescript await sms.optOuts.check('+14155551234'); // Returns boolean await sms.optOuts.add('+14155551234'); await sms.optOuts.remove('+14155551234'); ``` ### Utilities ```typescript import { calculateSegments, validatePhoneNumber } from '@wraps.dev/sms'; calculateSegments('Hello!'); // 1 calculateSegments('a'.repeat(200)); // 2 validatePhoneNumber('+14155551234', 'to'); // Throws if invalid ``` ### sms.numbers.get(phoneNumberId) ```typescript const number = await sms.numbers.get('phone-number-id'); // Returns: { phoneNumberId, phoneNumber, numberType, messageType, twoWayEnabled } ``` ### sms.optOuts.list(options?) ```typescript const result = await sms.optOuts.list({ limit: 20 }); // Returns: { entries: OptOutEntry[], nextToken?: string } ``` ### Error Types ```typescript import { SMSError, ValidationError, OptedOutError, RateLimitError } from '@wraps.dev/sms'; // ValidationError: invalid input (.field, .message) // OptedOutError: user opted out (.phoneNumber) // SMSError: AWS error (.code, .retryable) // RateLimitError: rate limit exceeded (.retryAfter in seconds) ``` ### SMS Utilities ```typescript import { sanitizePhoneNumber } from '@wraps.dev/sms'; sanitizePhoneNumber('(415) 555-1234'); // '+14155551234' ``` Note: Default messageType is TRANSACTIONAL. Batch sends are sequential (not parallel). ## Platform SDK Reference (@wraps.dev/client) ### defineConfig() ```typescript import { defineConfig } from '@wraps.dev/client'; export default defineConfig({ org: 'your-org-slug', from: 'hello@yourdomain.com', replyTo: 'support@yourdomain.com', region: 'us-east-1', environments: { staging: { from: 'staging@yourdomain.com' } }, defaultEnv: 'production', templatesDir: './templates', workflowsDir: './workflows', brandFile: './brand.ts', preview: { port: 3333 }, }); ``` ### defineBrand() ```typescript import { defineBrand } from '@wraps.dev/client'; export default defineBrand({ companyName: 'Your Company', colors: { primary: '#6366f1', secondary: '#a5b4fc', background: '#ffffff', text: '#1f2937', muted: '#9ca3af' }, fonts: { heading: 'Inter, sans-serif', body: 'Inter, sans-serif' }, buttonStyle: { borderRadius: '6px', padding: '12px 24px' }, logoUrl: 'https://yourdomain.com/logo.png', address: '123 Main St, San Francisco, CA 94102', socialLinks: { twitter: 'https://twitter.com/yourcompany' }, }); ``` ### defineWorkflow() ```typescript import { defineWorkflow, sendEmail, delay, condition, exit } from '@wraps.dev/client'; export default defineWorkflow({ name: 'Welcome Sequence', trigger: { type: 'contact_created' }, settings: { allowReentry: false, contactCooldownSeconds: 86400 }, steps: [ sendEmail('welcome', { template: 'welcome-email' }), delay('wait', { days: 1 }), condition('check', { field: 'contact.hasActivated', operator: 'equals', value: true, branches: { yes: [exit('done')], no: [sendEmail('reminder', { template: 'activate' })] } }), ], }); ``` ### Workflow Step Helpers - `sendEmail(id, { template, from?, fromName?, replyTo? })` - Send email - `sendSms(id, { template?, message?, senderId? })` - Send SMS - `delay(id, { days?, hours?, minutes? })` - Wait - `condition(id, { field, operator, value, branches })` - Branch - `waitForEvent(id, { eventName, timeout? })` - Wait for event - `waitForEmailEngagement(id, { emailStepId, engagementType, timeout? })` - Wait for open/click - `exit(id, { reason?, markAs? })` - End workflow - `updateContact(id, { updates })` - Modify contact fields (set, increment, decrement, append, remove) - `subscribeTopic(id, { topicId, channel? })` - Subscribe to topic - `unsubscribeTopic(id, { topicId, channel? })` - Unsubscribe from topic - `webhook(id, { url, method?, headers?, body? })` - External webhook - `cascade(id, { channels })` - Cross-channel cascade (see below) ### Trigger Types - `contact_created` - New contact added - `contact_updated` - Contact fields changed - `event` - Custom event received (requires `eventName`) - `segment_entry` - Contact enters segment (requires `segmentId`) - `segment_exit` - Contact exits segment (requires `segmentId`) - `schedule` - Cron-based schedule (requires `schedule` cron expression, optional `timezone`) - `api` - Triggered via API call - `topic_subscribed` - Contact subscribes to topic (requires `topicId`) - `topic_unsubscribed` - Contact unsubscribes from topic (requires `topicId`) ### Workflow Settings ```typescript settings?: { allowReentry?: boolean, // Allow same contact to re-enter reentryDelaySeconds?: number, // Min seconds between re-entries maxConcurrentExecutions?: number, // Max parallel executions contactCooldownSeconds?: number, // Cooldown before re-trigger } ``` ### cascade() — Cross-Channel Orchestration Tries channels in order; stops when engagement is detected. Returns `StepDefinition[]`. ```typescript import { defineWorkflow, cascade, sendEmail } from '@wraps.dev/client'; export default defineWorkflow({ name: 'Notify User', trigger: { type: 'event', eventName: 'payment_failed' }, steps: [ ...cascade('notify', { channels: [ { type: 'email', template: 'payment-failed', waitFor: { hours: 2 }, engagement: 'opened' }, { type: 'sms', template: 'payment-failed-sms' }, ], }), ], }); // Expands to: sendEmail → waitForEmailEngagement → condition (engaged?) → yes: exit → no: sendSms ``` ## Authentication All SDKs follow a 4-step credential resolution chain (in priority order): ### 1. Pre-configured AWS client (highest priority) ```typescript import { SESClient } from '@aws-sdk/client-ses'; const sesClient = new SESClient({ /* custom config */ }); const email = new WrapsEmail({ client: sesClient }); // Bypasses all other credential resolution ``` ### 2. OIDC role assumption (Vercel, EKS, GitHub Actions) ```typescript const email = new WrapsEmail({ roleArn: process.env.AWS_ROLE_ARN, roleSessionName: 'my-app-session', // Optional, defaults to 'wraps-email-session' }); // Uses AssumeRoleWithWebIdentity via AWS_WEB_IDENTITY_TOKEN_FILE ``` ### 3. Explicit credentials (static or provider function) ```typescript // Static credentials const email = new WrapsEmail({ credentials: { accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, }, region: 'us-east-1', }); // Credential provider function (e.g., from Vercel OIDC helper) const email = new WrapsEmail({ credentials: myCredentialProvider, // AwsCredentialIdentityProvider }); ``` ### 4. AWS SDK default chain (lowest priority, recommended) ```typescript const email = new WrapsEmail(); // Resolves: env vars -> ~/.aws/credentials -> ECS container -> EC2 instance metadata ``` ## Common Workflows ### 1. Deploy email and send first message ```bash npx @wraps.dev/cli email init npx @wraps.dev/cli email domains add -d yourapp.com npx @wraps.dev/cli email domains get-dkim -d yourapp.com # Add CNAME records to DNS provider npx @wraps.dev/cli email domains verify -d yourapp.com npm install @wraps.dev/email ``` ### 2. Deploy SMS and send first message ```bash npx @wraps.dev/cli sms init npx @wraps.dev/cli sms verify-number --phoneNumber +14155551234 npx @wraps.dev/cli sms test --to +14155551234 --message "Hello!" npm install @wraps.dev/sms ``` ### 3. Run deliverability audit ```bash npx @wraps.dev/cli email check yourapp.com # Checks DKIM, SPF, DMARC, MX TLS, blacklists ``` ### 4. Connect existing SES and upgrade ```bash npx @wraps.dev/cli email connect npx @wraps.dev/cli email upgrade ``` ## Configuration Presets ### Email | Preset | Monthly Cost | Features | |--------|-------------|----------| | starter | ~$0.05 | Open/click tracking, bounce/complaint suppression | | production | ~$2-5 | + Real-time event tracking, 90-day history, reputation metrics | | enterprise | ~$50-100 | + Dedicated IP, 1-year history, all 10 SES event types | ### SMS | Preset | Monthly Cost | Features | |--------|-------------|----------| | starter | ~$1 | Simulator phone number | | production | ~$2-10 | Toll-free number, event tracking | | enterprise | ~$10-50 | Full features, link tracking | ## Pricing | | Free | Starter | Growth | Scale | |--|------|---------|--------|-------| | **Price** | $0 | $10/mo | $49/mo | $149/mo | | **Dashboard** | Self-host | Hosted | Hosted | Hosted | | **Contacts** | Unlimited | Unlimited | Unlimited | Unlimited | | **Workflows** | -- | 5 | 25 | Unlimited | | **Features** | CLI/SDK | + Templates, Analytics | + Topics, Segments, Campaigns | + Events, Advanced Segments | You pay Wraps for tooling. You pay AWS for infrastructure. No surprises. **Example**: 100K emails/month = $10-49 (Wraps) + ~$10 (AWS) = $20-59 total. Managed services charge $80-150+ for the same volume. ## Available Packages - `@wraps.dev/email` - Email sending via AWS SES - `@wraps.dev/sms` - SMS via AWS End User Messaging - `@wraps.dev/cdn` - Asset delivery via AWS CloudFront - `@wraps.dev/cli` - Infrastructure deployment CLI ## Technical Stack - Infrastructure: Pulumi deploying to AWS (SES, DynamoDB, Lambda, EventBridge, SQS, CloudFront, S3) - SDK: TypeScript, works with any Node.js framework - Platform: Next.js 15, React 19, shadcn/ui, Elysia API - Database: PostgreSQL (Neon) + Drizzle ORM - Auth: Better Auth with OIDC (Vercel, AWS native) ## Use Cases - Transactional emails (welcome, password reset, receipts) - Marketing broadcasts with segmentation - Automated email workflows (drip campaigns, onboarding) - SMS verification codes and notifications - CDN for email assets and hosted images - Developer applications needing full AWS control ## Environment Variables ### Wraps Configuration - `WRAPS_LOCAL_ONLY` - Disable telemetry and API calls (default: false) - `WRAPS_API_KEY` - API key for Wraps Platform - `WRAPS_API_URL` - Custom API endpoint (default: https://api.wraps.dev) - `WRAPS_TELEMETRY_DISABLED` - Disable anonymous telemetry - `WRAPS_HOME` - Custom config directory (default: ~/.wraps) ### AWS Credentials - `AWS_ACCESS_KEY_ID` - AWS access key - `AWS_SECRET_ACCESS_KEY` - AWS secret key - `AWS_SESSION_TOKEN` - Temporary session token - `AWS_REGION` - AWS region (default: us-east-1) - `AWS_PROFILE` - Named AWS CLI profile - `AWS_ROLE_ARN` - IAM role ARN for OIDC assumption ### DNS Automation - `CLOUDFLARE_API_TOKEN` - Cloudflare API token - `CLOUDFLARE_ZONE_ID` - Cloudflare zone ID - `VERCEL_TOKEN` - Vercel API token for DNS ## Infrastructure: What Gets Deployed ### Email (wraps email init) **All presets:** IAM role (OIDC trust), SES configuration set, email identity with DKIM **Production preset adds:** EventBridge rule, SQS queue + DLQ, Lambda processor (Node.js 20, 128MB), DynamoDB table (90-day TTL) **Enterprise preset adds:** Dedicated IP address, 365-day history, all 10 SES event types Architecture: SES → EventBridge → SQS + DLQ → Lambda → DynamoDB All resources tagged with ManagedBy: 'wraps-cli', prefixed with 'wraps-email-' ### SMS (wraps sms init) IAM role, phone number (simulator or toll-free), opt-out list, event tracking ### CDN (wraps cdn init) S3 bucket (private, versioned), CloudFront distribution (OAI, HTTPS), optional ACM certificate + custom domain ## Links - Website: https://wraps.dev - Quickstart: https://wraps.dev/docs/quickstart/email - Next.js Guide: https://wraps.dev/docs/quickstart/email/nextjs - Express Guide: https://wraps.dev/docs/quickstart/email/express - Remix Guide: https://wraps.dev/docs/quickstart/email/remix - Email SDK Reference: https://wraps.dev/docs/sdk-reference - SMS SDK Reference: https://wraps.dev/docs/sms-sdk-reference - Platform SDK Reference: https://wraps.dev/docs/client-sdk-reference - CLI Reference: https://wraps.dev/docs/cli-reference - Auth Commands: https://wraps.dev/docs/cli-reference/auth - AWS Commands: https://wraps.dev/docs/cli-reference/aws - Platform Commands: https://wraps.dev/docs/cli-reference/platform - Infrastructure (Email): https://wraps.dev/docs/infrastructure/email - Infrastructure (SMS): https://wraps.dev/docs/infrastructure/sms - Infrastructure (CDN): https://wraps.dev/docs/infrastructure/cdn - Error Codes: https://wraps.dev/docs/reference/errors - Environment Variables: https://wraps.dev/docs/reference/environment-variables - Configuration Presets: https://wraps.dev/docs/guides/configuration-presets - Templates Guide: https://wraps.dev/docs/guides/templates - Workflows Guide: https://wraps.dev/docs/guides/workflows - Orchestration Guide: https://wraps.dev/docs/guides/orchestration - Webhooks Guide: https://wraps.dev/docs/guides/webhooks - GitHub (SDK): https://github.com/wraps-team/wraps-js - npm (email): https://npmjs.com/package/@wraps.dev/email - npm (sms): https://npmjs.com/package/@wraps.dev/sms - npm (client): https://npmjs.com/package/@wraps.dev/client - npm (cli): https://npmjs.com/package/@wraps.dev/cli ## Contact - Support: support@wraps.dev - Twitter: @useWraps