Infrastructure Intents
Infrastructure intents let you declare what resources your application needs without specifying provider-specific details. VentureKit translates intents into AWS resources (RDS, S3, SQS, ElastiCache, etc.) at deploy time.
Why Intents?
Section titled “Why Intents?”Traditional IaC (Infrastructure as Code) requires you to know AWS service names, instance types, and configuration parameters. Intents abstract this:
- CDK updates only require updating
@venturekit/infra— consumer code stays the same - Stable API for consuming projects — intents don’t change when CDK does
- Simplified configuration — no need to know AWS instance types
Declaring Intents
Section titled “Declaring Intents”Define what resources exist once in vk.config.ts (names, types, purposes). Then override sizing and behavior per environment in config/dev.ts and config/prod.ts.
Shared definitions (vk.config.ts)
Section titled “Shared definitions (vk.config.ts)”import { defineVenture } from '@venturekit/infra';import { base } from './config/base';import { dev } from './config/dev';import { prod } from './config/prod';
export default defineVenture({ base, databases: [ { id: 'main', type: 'postgres', name: 'app' }, ], storage: [ { id: 'uploads', purpose: 'uploads' }, ], auth: [ { id: 'main', signInWith: ['email'], allowSignUp: true }, ], queues: [ { id: 'jobs', type: 'standard' }, ], envs: { dev, prod },});The full list of intent arrays on defineVenture() (all optional): databases, storage, auth, queues, caches, schedules, functions, monitoring, secrets, envVars, domains, notify, events, billing. Every one of them can be overridden per environment by id.
Per-environment overrides
Section titled “Per-environment overrides”Each env config overrides only what differs. Items are matched by id and shallow-merged with the shared definition:
import type { EnvConfigInput } from '@venturekit/core';
export const dev: EnvConfigInput = { preset: 'free', databases: [ { id: 'main', size: 'small' }, ],};import type { EnvConfigInput } from '@venturekit/core';
export const prod: EnvConfigInput = { preset: 'medium', databases: [ { id: 'main', size: 'large', highAvailability: true, backups: true }, ], storage: [ { id: 'uploads', cdn: true }, ], queues: [ { id: 'jobs', deadLetterQueue: true }, ],};At deploy time, VentureKit merges the shared definitions with the active environment’s overrides and provisions the corresponding AWS resources.
Intent Types
Section titled “Intent Types”Database Intent
Section titled “Database Intent”{ id: 'main-db', type: 'postgres', // 'postgres' | 'mysql' size: 'small', // 'small' | 'medium' | 'large' | 'xlarge' name: 'app_main', highAvailability: true, // Multi-AZ backups: true, backupRetentionDays: 7, encrypted: true, performanceInsights: true, proxy: true, // opt-in RDS Proxy — see below; `true` or the tunables object}proxyputs an RDS Proxy in front of the instance. It is off by default and costs money — about $0.015 per vCPU-hour of the target instance, billed for at least 2 vCPUs (≈ $22/month per proxied database) on top of the instance. What it buys is connection pooling across Lambda containers: each warm container holds its ownpgpool (DB_POOL_MAX, default 4 in Lambda), so a burst of N containers opens up to 4·N connections againstmax_connections(~100 on asmallinstance); the proxy multiplexes them onto a bounded set and absorbs Multi-AZ failover. Consider it once a stage regularly runs more than ~20 concurrent containers against a small class, or beforehighAvailability. Wiring is transparent —DB_HOSTbecomes the proxy endpoint (same port, same credentials secret, TLS required; the vendored RDS CA covers proxy endpoints), the Lambda security group is admitted to the proxy’s and the proxy’s to the instance’s, while the in-VPC migration runner keeps talking to the instance directly. The object form tunes it:{ idleClientTimeoutSeconds: 1800, maxConnectionsPercent: 90, borrowTimeoutSeconds: 30 }(defaults shown).
Storage Intent
Section titled “Storage Intent”{ id: 'uploads', purpose: 'uploads', // 'uploads' | 'assets' | 'backups' | 'logs' cdn: true, // Enable CloudFront cdnDomain: 'cdn.example.com', versioned: true, corsOrigins: ['https://app.example.com'],}Auth Intent
Section titled “Auth Intent”Declares a Cognito User Pool without exposing Cognito configuration. Two shapes share the auth array: an owned pool this venture creates, or an external pool another venture owns whose tokens this venture verifies.
// Owned pool — the common case{ id: 'main', signInWith: ['email'], // Array<'email' | 'phone' | 'username'> allowSignUp: true, mfa: 'optional', // 'off' (default) | 'optional' | 'required' — authenticator app (TOTP) passwordStrength: 'strong', // 'standard' | 'strong' clients: [{ name: 'web' }, { name: 'mobile' }], // one app client per entry; default: a single 'default' client tokenValidity: { accessMinutes: 15, idMinutes: 15, refreshDays: 365 }, customAttributes: ['plan'], // your own custom:* claims — feature-implied ones are added for you tenancy: true, // multi-tenant pool: auto-declares custom:tenantId + custom:tenantRoles federated: ['google', 'linkedin'], // 'google' | 'facebook' | 'apple' | 'linkedin' passkeys: { relyingPartyId: 'example.com', userVerification: 'preferred' }, // WebAuthn; essentials/plus plans only adminRoutes: ['/auth/**', '/admin/users/**', 'fn:cognito-user-migration'], // Lambdas allowed to call Cognito admin APIs customSender: { email: 'auth-mailer' }, // functions[] id that becomes the CustomEmailSender trigger}
// External pool — trust a pool owned by another venture (same account/region/stage){ id: 'staff', external: { project: 'control-plane', intent: 'main' }, // intent defaults to 'main' adminRoutes: ['/staff/**'], // same semantics as on an owned pool}- Scopes are not declared here. Authorization is enforced server-side (
scopes: [...]on a handler) — cookie’d ID tokens never carry OAuth scope claims, so there are no Cognito resource servers to configure. - MFA is always an authenticator app. SMS is never enabled and email OTP is not offered yet. Enrolment/verification helpers live in
@venturekit/auth/server(beginTotpSetup,confirmTotpSetup,respondToSoftwareTokenMfa, …) — see the Authentication guide. federateddoes not enable the Hosted UI: your SPA does the OAuth dance with the provider, posts the resulting token to your API, and your handler callssignInAsFederatedUser(). Each provider gets a Secrets Manager placeholder atventurekit/<project>/<stage>/auth/<id>/<provider>(exposed asCOGNITO_FEDERATED_<PROVIDER>_SECRET_ARN) that you populate after the first deploy, plus auto-declaredfederated_providerand<provider>Subcustom attributes.passkeysadds WebAuthn as a sign-in factor — opt-in, no extra AWS cost, but only on theessentials/plusfeature plan (themedium/largepresets); on alitepool the deploy fails at synth with a fix-it message.relyingPartyIdis the apex domain every enrolled passkey is bound to (example.comcoversapp.example.comandadmin.example.com; changing it invalidates every passkey), and it must be a bare domain, not a URL.userVerification: 'required'insists on the device’s biometric / PIN;'preferred'(default) lets the authenticator decide. Provisions the pool’s relying party, the choice-basedUSER_AUTHflow (password stays enabled) andALLOW_USER_AUTHon every app client;@venturekit/auth/server’sbeginPasskeyRegistration/completePasskeySignInfamily drives the flows — see the Authentication guide.adminRoutesnames the Lambdas allowed to call Cognito’s admin APIs on the pool (AdminCreateUser,AdminSetUserPassword,AdminInitiateAuth, …) — the ones runningsignUpUser,signInAsFederatedUser,adminInviteUser, the passwordless and forgot-password flows. When declared,vk deployprovisions a second execution role carrying those grants and attaches it only to the matching functions; every other Lambda keeps the shared role without Cognito admin powers, so a bug in an unrelated public route cannot become “create an admin user”. Entries match the route’s API path (*one segment,**any number —'/auth/**'covers every auth route); standalonefunctions[],queues[]consumers andschedules[]handlers match by id as'fn:<id>'. Omitted, you get the historical layout — one shared role with the admin grants — and a synth-time warning. Err on the side of'/auth/**'plus every admin surface that manages users: an excluded route fails withAccessDeniedExceptionat first call.customAttributesare created as mutable strings.roleis auto-declared unlesstenancy: true, in which casetenantId+tenantRolesreplace it (resolveAuthCustomAttributes()in@venturekit/coreis the exact rule).customSenderroutes the messages Cognito would send itself (verification and forgot-password codes, MFA OTPs, temporary passwords) through your ownsrc/functions/<id>.tshandler.vk deployprovisions the KMS key (alias/<project>-<stage>-<id>-cognito-custom-sender), the invoke permission and the pool’sLambdaConfig; the key ARN reaches every Lambda asAUTH_<ID>_CUSTOM_SENDER_KMS_KEY_ARN. A custom sender takes over every message of its channel. Declaringsmsalso provisions the pool’s SNS SMS role. Ignored undervk dev(cognito-local cannot invoke Lambdas).- Runtime env vars. Every intent, owned or external, is exposed as
AUTH_<ID>_USER_POOL_ID,AUTH_<ID>_APP_CLIENT_ID,AUTH_<ID>_REGION(+AUTH_<ID>_ENDPOINTundervk dev), with<ID>upper-snaked. The first declared intent is also the canonicalCOGNITO_*set thatloadAuthServerConfig()reads by default. - The pool’s per-MAU feature plan is an environment setting, not an intent field:
identity: { featurePlan: 'lite' | 'essentials' | 'plus' }inconfig/<env>.ts(presets chooseliteoressentials;plus— threat protection, no free tier — is always opt-in). - The local pool under
vk devis a cognito-local container provisioned with the same schema, so locally issued JWTs carry the same custom claims.tokenValidityis not honoured locally.
Queue Intent
Section titled “Queue Intent”{ id: 'email-queue', type: 'standard', // 'standard' | 'fifo' deadLetterQueue: true, retentionDays: 7, visibilityTimeoutSeconds: 60,}Cache Intent
Section titled “Cache Intent”{ id: 'session-cache', type: 'redis', // 'redis' | 'memcached' size: 'small', // 'small' | 'medium' | 'large' | 'xlarge' clustered: false,}Schedule Intent
Section titled “Schedule Intent”The handler is auto-discovered at src/crons/<id>.ts; handler only needs setting when the file lives elsewhere.
// Rate-based{ id: 'cleanup-job', // → src/crons/cleanup-job.ts schedule: { rate: '1 hour' }, // "5 minutes", "1 hour", "1 day" enabled: true, // default true; false never registers the job (locally too) memorySize: 256, // MB, default 256 timeout: 60, // seconds, default 60}
// Cron-based (EventBridge 6-field form){ id: 'daily-report', schedule: { cron: '0 8 * * ? *' }, // 8:00 AM UTC daily}vk dev runs the same schedules locally (per-env overrides included) and exposes each handler at POST /_dev/invoke/cron/{id}; pass --no-crons to keep the scheduler dormant while still allowing manual invocation.
Function Intent
Section titled “Function Intent”A standalone Lambda that is not a route, queue consumer or cron — saga workers, data processors, Cognito custom senders. Discovered from src/functions/: src/functions/process-orders.ts → {project}-{stage}-fn-process-orders, src/functions/order/create-order.ts → ...-fn-order-create-order. Invoke with invoke({ function: 'process-orders' }) from @venturekit/runtime/patterns.
{ id: 'process-orders', // matches the file path under src/functions/ memorySize: 512, // default 256 timeout: 120, // seconds, default 30 description: 'Nightly order reconciliation',}Events Intent
Section titled “Events Intent”A domain-event bus (EventBridge custom bus) plus the rules that route matched events to in-venture targets — the pub/sub complement to point-to-point queues. One published event can fan out to many subscribers, including subscribers in other ventures.
// Owner: creates bus {project}-{stage}-domain, grants events:PutEvents,// injects VENTURE_EVENT_BUS_NAME + EVENT_BUS_DOMAIN_NAME{ id: 'domain', source: 'control-plane', // default source stamped on published events (defaults to the project name) subscriptions: [ { id: 'tenant-lifecycle', detailTypes: ['tenant.created', 'tenant.suspended'], target: { queue: 'tenant-events' }, // a queues[] id — or { function: '<functions[] id>' } }, ],}
// Subscriber only: attach rules to a bus another venture owns (same account + region; owner deploys first){ id: 'platform', external: { busName: 'control-plane-prod-domain' }, subscriptions: [ { id: 'schools', sources: ['control-plane'], pattern: { detail: { vertical: ['school'] } }, target: { function: 'sync-school' } }, ],}Handlers publish with publishEvent() / publishEvents() from @venturekit/runtime/patterns; queue targets receive the event as an ordinary SQS message (at-least-once), function targets are invoked asynchronously.
Domain Intent
Section titled “Domain Intent”Custom domains for the HTTP API (default) or the WebSocket API. VentureKit does not assume Route 53 — the default dns: 'external' creates no DNS records.
{ id: 'api', domain: 'api.example.com', target: 'http', // 'http' (edge stack) | 'websocket' (requires websocket.enabled + src/ws/) dns: 'external', // 'external' (default) | 'route53' (requires hostedZoneId) hostedZoneId: 'Z123ABCEXAMPLE', // route53 mode only certificateArn: 'arn:aws:acm:...', // optional — see below basePath: '/', // API mapping path, default '/'}Certificate resolution: with certificateArn the cert is imported; with dns: 'route53' a DNS-validated cert is issued against the zone; with external DNS and no ARN, vk deploy finds or requests a DNS-validated cert in ACM, prints the validation CNAMEs, and only creates the custom domain once ACM reports ISSUED — the API stays reachable on its execute-api URL meanwhile, and a domain that is skipped is not published as edgeKeys.apiCustomDomain / VENTURE_WS_CUSTOM_DOMAIN. vk cert request does the same up front and writes the ARN into your config so CI never depends on the lookup. WebSocket certs are issued in the deploy region (not us-east-1).
Billing Intent
Section titled “Billing Intent”Declares a payment provider the app transacts through. No credentials in config: VentureKit provisions a placeholder secret in Secrets Manager and injects its ARN; an operator fills in the real values after the first deploy.
{ id: 'main', provider: 'zazu', // provisions {apiKey, accountId, webhookSecret}; injects ZAZU_SECRET_ARN}The runtime client lives in @venturekit/billing.
Secrets & Environment Variables
Section titled “Secrets & Environment Variables”secrets: [ { id: 'stripe-key', store: 'secretsmanager', encrypted: true }, // 'ssm' | 'secretsmanager'],envVars: [ { name: 'FEATURE_FLAGS', value: 'beta' }, // plain value… { name: 'STRIPE_KEY', secretRef: 'stripe-key' }, // …or a reference to a secrets[] id (mutually exclusive)],envVars are injected into every Lambda; secrets become outputs.secrets['<id>'].arn / .name.
Monitoring Intent
Section titled “Monitoring Intent”{ id: 'api-health', dashboard: true, // CloudWatch dashboard notificationArn: 'arn:aws:sns:...', alarms: [ { name: 'high-5xx', metric: '5XXError', namespace: 'AWS/ApiGateway', // default AWS/Lambda threshold: 10, comparison: 'gt', // 'gt' | 'gte' | 'lt' | 'lte' evaluationPeriods: 2, // default 1 periodSeconds: 300, // default 300 statistic: 'Sum', // 'Average' | 'Sum' | 'Minimum' | 'Maximum' | 'SampleCount' dimensions: { ApiName: 'my-api' }, }, ],}Notify Intent
Section titled “Notify Intent”Declares the application’s transactional-messaging surface (email / WhatsApp /
SMS / push). VentureKit provisions an SES identity + configuration set, a
bounce/complaint SNS topic, an outbox table in your Postgres database, and a
dispatcher cron — your handlers just call notify.enqueue(...) (see the
@venturekit/notify package).
{ id: 'main', channels: ['email', 'whatsapp'], // 'email' | 'whatsapp' | 'sms' | 'push' defaultFrom: 'no-reply@app.example.com', domain: 'app.example.com', // SES DomainIdentity with DKIM signing sesIdentity: 'own', // 'own' | 'shared' — omit and vk deploy decides (see below) hostedZoneId: 'Z123ABCEXAMPLE', // auto-publish DKIM CNAMEs in Route 53 (ignored when shared) tracking: 'postgres', // 'postgres' (default) | 'none' databaseId: 'main', // outbox database — required only with several databases[] clientModule: './src/lib/notify.ts', // module exporting `notify` (default shown) dispatchRateMinutes: 1, // dispatcher poll rate dispatchBatchSize: 50, // notifications claimed per invocation whatsapp: { phoneNumberId: '1234567890', // Meta Cloud API Phone Number ID wabaId: '9876543210', // WhatsApp Business Account ID — needed by the template-management APIs apiVersion: 'v21.0', webhooks: true, // also provision the App Secret + verify-token secrets for inbound webhooks },}Most projects need exactly one notify intent (id: 'main'). The WhatsApp
access token is stored in a Secrets Manager secret created by the stack —
populate it after the first deploy. The runtime client that consumes this
intent ships in the free @venturekit/notify package.
sesIdentity— an SES identity is one per account + region, so two ventures sending from the same domain cannot both create it.'shared'creates noEmailIdentity(and no DKIM records) but still provisions this venture’s own configuration set, events topic and DLQs. Leave it unset andvk deploy’s pre-flight asks SES whether the identity exists and whether this venture’s<project>-<stage>-messagingstack owns it. Under strictdataSafetythe identity is retained on stack deletion.whatsapp.webhooks— opt-in. Provisions two extra secrets whose ARNs reach Lambdas asVENTURE_NOTIFY_WHATSAPP_APP_SECRETandVENTURE_NOTIFY_WHATSAPP_VERIFY_TOKEN_SECRET, socreateMetaWhatsAppWebhookHandler()needs no credential arguments. Declaring it without'whatsapp'inchannelsis a synth-time error.wabaIdis exported asVENTURE_NOTIFY_WHATSAPP_WABA_ID.
Resource Sizes
Section titled “Resource Sizes”| Size | DB Instance | Cache Node |
|---|---|---|
small | db.t3.small | cache.t3.small |
medium | db.r6g.large | cache.r6g.large |
large | db.r6g.xlarge | cache.r6g.xlarge |
xlarge | db.r6g.2xlarge | cache.r6g.2xlarge |
Accessing Intent Outputs
Section titled “Accessing Intent Outputs”After infrastructure is provisioned, outputs are available in route handlers via ctx.intentOutputs:
import { handler } from '@venturekit/runtime';import type { IntentOutputs } from '@venturekit/core';
export const main = handler(async (_body, ctx, logger) => { const outputs = ctx.intentOutputs as IntentOutputs;
// Database const dbEndpoint = outputs.databases['main-db'].endpoint; const dbPort = outputs.databases['main-db'].port; const dbSecretArn = outputs.databases['main-db'].secretArn;
// Storage const bucketName = outputs.storage['uploads'].bucketName; const cdnUrl = outputs.storage['uploads'].cdnUrl;
// Queue const queueUrl = outputs.queues['email-queue'].queueUrl;
// Cache const cacheEndpoint = outputs.caches['session-cache'].endpoint;
// Auth const userPoolId = outputs.auth['main'].userPoolId; const clientId = outputs.auth['main'].clientId;
// Functions, event buses, secrets const fnArn = outputs.functions['process-orders'].functionArn; const busName = outputs.eventBuses['domain'].busName; const secretArn = outputs.secrets['stripe-key'].arn;
// Notify (SES + dispatcher) const sesConfigSet = outputs.notify['main'].configurationSetName; const bounceTopicArn = outputs.notify['main'].eventsTopicArn;
// Billing (placeholder credential secret) const zazuSecretArn = outputs.billing['main'].secretArn;
return { dbEndpoint, bucketName, queueUrl };});Intent outputs are serialized into the VENTURE_INTENT_OUTPUTS Lambda environment variable and hydrated automatically into ctx.intentOutputs at runtime.