@friggframework/devtools 2.0.0--canary.463.62579dd.0 → 2.0.0--canary.461.ec909cf.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs');
3
3
  const { AWSDiscovery } = require('./aws-discovery');
4
+ const { buildPrismaLayer } = require('./scripts/build-prisma-layer');
4
5
 
5
6
  const shouldRunDiscovery = (AppDefinition) => {
6
7
  console.log(
@@ -17,7 +18,8 @@ const shouldRunDiscovery = (AppDefinition) => {
17
18
  return (
18
19
  AppDefinition.vpc?.enable === true ||
19
20
  AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
20
- AppDefinition.ssm?.enable === true
21
+ AppDefinition.ssm?.enable === true ||
22
+ AppDefinition.database?.postgres?.enable === true
21
23
  );
22
24
  };
23
25
 
@@ -68,8 +70,7 @@ const getAppEnvironmentVars = (AppDefinition) => {
68
70
  }
69
71
  if (skippedKeys.length > 0) {
70
72
  console.log(
71
- ` ⚠️ Skipped ${
72
- skippedKeys.length
73
+ ` ⚠️ Skipped ${skippedKeys.length
73
74
  } reserved AWS Lambda variables: ${skippedKeys.join(', ')}`
74
75
  );
75
76
  }
@@ -230,7 +231,26 @@ const createVPCInfrastructure = (AppDefinition) => {
230
231
  Tags: [
231
232
  {
232
233
  Key: 'Name',
233
- Value: '${self:service}-${self:provider.stage}-public-subnet',
234
+ Value: '${self:service}-${self:provider.stage}-public-subnet-1',
235
+ },
236
+ { Key: 'ManagedBy', Value: 'Frigg' },
237
+ { Key: 'Service', Value: '${self:service}' },
238
+ { Key: 'Stage', Value: '${self:provider.stage}' },
239
+ { Key: 'Type', Value: 'Public' },
240
+ ],
241
+ },
242
+ },
243
+ FriggPublicSubnet2: {
244
+ Type: 'AWS::EC2::Subnet',
245
+ Properties: {
246
+ VpcId: { Ref: 'FriggVPC' },
247
+ CidrBlock: '10.0.4.0/24',
248
+ AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
249
+ MapPublicIpOnLaunch: true,
250
+ Tags: [
251
+ {
252
+ Key: 'Name',
253
+ Value: '${self:service}-${self:provider.stage}-public-subnet-2',
234
254
  },
235
255
  { Key: 'ManagedBy', Value: 'Frigg' },
236
256
  { Key: 'Service', Value: '${self:service}' },
@@ -341,6 +361,13 @@ const createVPCInfrastructure = (AppDefinition) => {
341
361
  RouteTableId: { Ref: 'FriggPublicRouteTable' },
342
362
  },
343
363
  },
364
+ FriggPublicSubnet2RouteTableAssociation: {
365
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
366
+ Properties: {
367
+ SubnetId: { Ref: 'FriggPublicSubnet2' },
368
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
369
+ },
370
+ },
344
371
  FriggPrivateRouteTable: {
345
372
  Type: 'AWS::EC2::RouteTable',
346
373
  Properties: {
@@ -544,10 +571,18 @@ const gatherDiscoveredResources = async (AppDefinition) => {
544
571
  try {
545
572
  const region = process.env.AWS_REGION || 'us-east-1';
546
573
  const discovery = new AWSDiscovery(region);
574
+ // Use Serverless Framework's stage resolution (opt:stage with 'dev' as default)
575
+ // This matches how serverless.yml resolves ${opt:stage, "dev"}
576
+ // IMPORTANT: Use SLS_STAGE (not STAGE) to match actual deployment stage
577
+ const stage = process.env.SLS_STAGE || 'dev';
578
+
547
579
  const config = {
548
580
  vpc: AppDefinition.vpc || {},
549
581
  encryption: AppDefinition.encryption || {},
550
582
  ssm: AppDefinition.ssm || {},
583
+ database: AppDefinition.database || {},
584
+ serviceName: AppDefinition.name || 'create-frigg-app',
585
+ stage: stage,
551
586
  };
552
587
 
553
588
  const discoveredResources = await discovery.discoverResources(config);
@@ -592,7 +627,9 @@ const buildEnvironment = (appEnvironmentVars, discoveredResources) => {
592
627
  defaultSecurityGroupId: 'AWS_DISCOVERY_SECURITY_GROUP_ID',
593
628
  privateSubnetId1: 'AWS_DISCOVERY_SUBNET_ID_1',
594
629
  privateSubnetId2: 'AWS_DISCOVERY_SUBNET_ID_2',
595
- publicSubnetId: 'AWS_DISCOVERY_PUBLIC_SUBNET_ID',
630
+ publicSubnetId: 'AWS_DISCOVERY_PUBLIC_SUBNET_ID', // Keep for backward compat
631
+ publicSubnetId1: 'AWS_DISCOVERY_PUBLIC_SUBNET_ID_1',
632
+ publicSubnetId2: 'AWS_DISCOVERY_PUBLIC_SUBNET_ID_2',
596
633
  defaultRouteTableId: 'AWS_DISCOVERY_ROUTE_TABLE_ID',
597
634
  defaultKmsKeyId: 'AWS_DISCOVERY_KMS_KEY_ID',
598
635
  };
@@ -603,6 +640,22 @@ const buildEnvironment = (appEnvironmentVars, discoveredResources) => {
603
640
  }
604
641
  }
605
642
 
643
+ // Add Aurora discovery mappings
644
+ if (discoveredResources.aurora) {
645
+ if (discoveredResources.aurora.clusterIdentifier) {
646
+ environment.AWS_DISCOVERY_AURORA_CLUSTER_ID = discoveredResources.aurora.clusterIdentifier;
647
+ }
648
+ if (discoveredResources.aurora.endpoint) {
649
+ environment.AWS_DISCOVERY_AURORA_ENDPOINT = discoveredResources.aurora.endpoint;
650
+ }
651
+ if (discoveredResources.aurora.port) {
652
+ environment.AWS_DISCOVERY_AURORA_PORT = discoveredResources.aurora.port.toString();
653
+ }
654
+ if (discoveredResources.aurora.secretArn) {
655
+ environment.AWS_DISCOVERY_AURORA_SECRET_ARN = discoveredResources.aurora.secretArn;
656
+ }
657
+ }
658
+
606
659
  return environment;
607
660
  };
608
661
 
@@ -613,20 +666,76 @@ const createBaseDefinition = (
613
666
  ) => {
614
667
  const region = process.env.AWS_REGION || 'us-east-1';
615
668
 
669
+ // Function-level package config to exclude Prisma and AWS SDK
670
+ // Uses native Serverless package.exclude since jetpack function-level config isn't supported in v3
671
+ const functionPackageConfig = {
672
+ exclude: [
673
+ // Exclude AWS SDK (already in Lambda runtime)
674
+ 'node_modules/aws-sdk/**',
675
+ 'node_modules/@aws-sdk/**',
676
+
677
+ // Exclude Prisma (provided via Lambda Layer)
678
+ 'node_modules/@prisma/**',
679
+ 'node_modules/.prisma/**',
680
+ 'node_modules/prisma/**',
681
+ 'node_modules/@friggframework/core/generated/**',
682
+
683
+ // Exclude nested node_modules from symlinked frigg packages (for npm link development)
684
+ 'node_modules/@friggframework/core/node_modules/**',
685
+ 'node_modules/@friggframework/devtools/node_modules/**',
686
+ ],
687
+ };
688
+
616
689
  return {
617
690
  frameworkVersion: '>=3.17.0',
618
691
  service: AppDefinition.name || 'create-frigg-app',
619
692
  package: {
620
693
  individually: true,
621
- exclude: [
622
- '!**/node_modules/aws-sdk/**',
623
- '!**/node_modules/@aws-sdk/**',
624
- '!package.json',
694
+ // NOTE: These patterns are NOT used when serverless-jetpack is enabled with trace mode
695
+ // Jetpack's trace mode completely overrides package.patterns during dependency resolution
696
+ // These are kept commented out as a fallback if Jetpack needs to be disabled
697
+ patterns: [
698
+ // AWS SDK exclusions (already in Lambda runtime)
699
+ // '!**/node_modules/aws-sdk/**',
700
+ // '!**/node_modules/@aws-sdk/**',
701
+
702
+ // Prisma exclusions (provided via Lambda Layer)
703
+ // '!**/node_modules/@prisma/**',
704
+ // '!**/node_modules/.prisma/**',
705
+ // '!**/node_modules/@prisma-mongodb/**',
706
+ // '!**/node_modules/@prisma-postgresql/**',
707
+ // '!**/node_modules/prisma/**',
708
+
709
+ // Exclude Prisma generated clients from @friggframework/core
710
+ // '!**/node_modules/@friggframework/core/generated/**',
711
+
712
+ // Exclude development and test files
713
+ // '!**/test/**',
714
+ // '!**/tests/**',
715
+ // '!**/*.test.js',
716
+ // '!**/*.spec.js',
717
+ // '!**/*.map',
718
+ // '!**/jest.config.js',
719
+ // '!**/jest.unit.config.js',
720
+ // '!**/.eslintrc.json',
721
+ // '!**/.prettierrc',
722
+ // '!**/.prettierignore',
723
+ // '!**/.markdownlintignore',
724
+ // '!**/docker-compose.yml',
725
+ // '!**/package.json',
726
+ // '!**/README.md',
727
+ // '!**/*.md',
728
+
729
+ // Exclude .DS_Store and other OS files
730
+ // '!**/.DS_Store',
731
+ // '!**/.git/**',
732
+ // '!**/.claude-flow/**',
625
733
  ],
626
734
  },
627
735
  useDotenv: true,
628
736
  provider: {
629
737
  name: AppDefinition.provider || 'aws',
738
+ ...(process.env.AWS_PROFILE && { profile: process.env.AWS_PROFILE }),
630
739
  runtime: 'nodejs20.x',
631
740
  timeout: 30,
632
741
  region,
@@ -697,13 +806,17 @@ const createBaseDefinition = (
697
806
  skipCacheInvalidation: false,
698
807
  },
699
808
  jetpack: {
700
- base: '..',
809
+ base: '..', // Essential for reaching handlers in node_modules/@friggframework
810
+ // NOTE: Service-level preInclude applies to EVERYTHING (functions + layers)
811
+ // We need to ONLY exclude from functions, not from the Prisma layer
812
+ // Solution: Apply exclusions at function level instead
701
813
  },
702
814
  },
703
815
  functions: {
704
816
  auth: {
705
- handler:
706
- 'node_modules/@friggframework/core/handlers/routers/auth.handler',
817
+ handler: 'node_modules/@friggframework/core/handlers/routers/auth.handler',
818
+ layers: [{ Ref: 'PrismaLambdaLayer' }],
819
+ package: functionPackageConfig,
707
820
  events: [
708
821
  { httpApi: { path: '/api/integrations', method: 'ANY' } },
709
822
  {
@@ -716,20 +829,52 @@ const createBaseDefinition = (
716
829
  ],
717
830
  },
718
831
  user: {
719
- handler:
720
- 'node_modules/@friggframework/core/handlers/routers/user.handler',
721
- events: [
722
- { httpApi: { path: '/user/{proxy+}', method: 'ANY' } },
723
- ],
832
+ handler: 'node_modules/@friggframework/core/handlers/routers/user.handler',
833
+ layers: [{ Ref: 'PrismaLambdaLayer' }],
834
+ package: functionPackageConfig,
835
+ events: [{ httpApi: { path: '/user/{proxy+}', method: 'ANY' } }],
724
836
  },
725
837
  health: {
726
- handler:
727
- 'node_modules/@friggframework/core/handlers/routers/health.handler',
838
+ handler: 'node_modules/@friggframework/core/handlers/routers/health.handler',
839
+ layers: [{ Ref: 'PrismaLambdaLayer' }],
840
+ package: functionPackageConfig,
728
841
  events: [
729
842
  { httpApi: { path: '/health', method: 'GET' } },
730
843
  { httpApi: { path: '/health/{proxy+}', method: 'GET' } },
731
844
  ],
732
845
  },
846
+ dbMigrate: {
847
+ handler: 'node_modules/@friggframework/core/handlers/workers/db-migration.handler',
848
+ // Uses Prisma Layer (includes CLI) - simpler than standalone packaging
849
+ layers: [{ Ref: 'PrismaLambdaLayer' }],
850
+ timeout: 300, // 5 minutes for long-running migrations
851
+ memorySize: 512, // Extra memory for Prisma CLI operations
852
+ reservedConcurrency: 1, // Prevent concurrent migrations
853
+ description: 'Runs database migrations via Prisma (invoke manually from CI/CD). Uses Prisma layer with CLI.',
854
+ package: functionPackageConfig, // Use same exclusions as other functions
855
+ // No events - this function is invoked manually via AWS CLI
856
+ maximumEventAge: 60, // Don't retry old migration requests (60 seconds)
857
+ maximumRetryAttempts: 0, // Don't auto-retry failed migrations
858
+ tags: {
859
+ Purpose: 'DatabaseMigration',
860
+ ManagedBy: 'Frigg',
861
+ },
862
+ // Environment variables for non-interactive Prisma CLI operation
863
+ environment: {
864
+ CI: '1', // Forces Prisma to non-interactive mode
865
+ PRISMA_HIDE_UPDATE_MESSAGE: '1', // Suppress update messages
866
+ PRISMA_MIGRATE_SKIP_SEED: '1', // Skip seeding during migrations
867
+ },
868
+ },
869
+ },
870
+ layers: {
871
+ prisma: {
872
+ path: 'layers/prisma',
873
+ name: '${self:service}-prisma-${sls:stage}',
874
+ description: 'Prisma ORM client with CLI and rhel-openssl-3.0.x binaries. Configured based on AppDefinition database settings. Used by all functions.',
875
+ compatibleRuntimes: ['nodejs18.x', 'nodejs20.x'],
876
+ retain: false, // Don't retain old layer versions
877
+ },
733
878
  },
734
879
  resources: {
735
880
  Resources: {
@@ -828,18 +973,22 @@ const applyKmsConfiguration = (
828
973
  }
829
974
 
830
975
  if (discoveredResources.defaultKmsKeyId) {
831
- console.log(
832
- `Using existing KMS key: ${discoveredResources.defaultKmsKeyId}`
833
- );
834
- definition.resources.Resources.FriggKMSKeyAlias = {
835
- Type: 'AWS::KMS::Alias',
836
- DeletionPolicy: 'Retain',
837
- Properties: {
838
- AliasName:
839
- 'alias/${self:service}-${self:provider.stage}-frigg-kms',
840
- TargetKeyId: discoveredResources.defaultKmsKeyId,
841
- },
842
- };
976
+ console.log(`Using existing KMS key: ${discoveredResources.defaultKmsKeyId}`);
977
+
978
+ // Only create alias if it doesn't already exist
979
+ if (!discoveredResources.kmsAliasExists) {
980
+ console.log('Creating KMS alias for discovered key...');
981
+ definition.resources.Resources.FriggKMSKeyAlias = {
982
+ Type: 'AWS::KMS::Alias',
983
+ DeletionPolicy: 'Retain',
984
+ Properties: {
985
+ AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
986
+ TargetKeyId: discoveredResources.defaultKmsKeyId,
987
+ },
988
+ };
989
+ } else {
990
+ console.log('KMS alias already exists, skipping alias creation');
991
+ }
843
992
 
844
993
  definition.provider.iamRoleStatements.push({
845
994
  Effect: 'Allow',
@@ -850,7 +999,7 @@ const applyKmsConfiguration = (
850
999
  if (AppDefinition.encryption?.createResourceIfNoneFound !== true) {
851
1000
  throw new Error(
852
1001
  'KMS field-level encryption is enabled but no KMS key was found. ' +
853
- 'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
1002
+ 'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
854
1003
  );
855
1004
  }
856
1005
 
@@ -889,9 +1038,8 @@ const applyKmsConfiguration = (
889
1038
  Resource: '*',
890
1039
  Condition: {
891
1040
  StringEquals: {
892
- 'kms:ViaService': `lambda.${
893
- process.env.AWS_REGION || 'us-east-1'
894
- }.amazonaws.com`,
1041
+ 'kms:ViaService': `lambda.${process.env.AWS_REGION || 'us-east-1'
1042
+ }.amazonaws.com`,
895
1043
  },
896
1044
  },
897
1045
  },
@@ -1218,7 +1366,34 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1218
1366
  Tags: [
1219
1367
  {
1220
1368
  Key: 'Name',
1221
- Value: '${self:service}-${self:provider.stage}-public',
1369
+ Value: '${self:service}-${self:provider.stage}-public-1',
1370
+ },
1371
+ { Key: 'Type', Value: 'Public' },
1372
+ { Key: 'ManagedBy', Value: 'Frigg' },
1373
+ ],
1374
+ },
1375
+ };
1376
+
1377
+ // Create second public subnet in different AZ for Aurora
1378
+ let publicSubnet2Cidr;
1379
+ if (vpcManagement === 'create-new') {
1380
+ const generatedCidrs = { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] };
1381
+ publicSubnet2Cidr = { 'Fn::Select': [3, generatedCidrs] };
1382
+ } else {
1383
+ publicSubnet2Cidr = '172.31.251.0/24';
1384
+ }
1385
+
1386
+ definition.resources.Resources.FriggPublicSubnet2 = {
1387
+ Type: 'AWS::EC2::Subnet',
1388
+ Properties: {
1389
+ VpcId: subnetVpcId,
1390
+ CidrBlock: publicSubnet2Cidr,
1391
+ MapPublicIpOnLaunch: true,
1392
+ AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
1393
+ Tags: [
1394
+ {
1395
+ Key: 'Name',
1396
+ Value: '${self:service}-${self:provider.stage}-public-2',
1222
1397
  },
1223
1398
  { Key: 'Type', Value: 'Public' },
1224
1399
  { Key: 'ManagedBy', Value: 'Frigg' },
@@ -1231,6 +1406,12 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1231
1406
  { Ref: 'FriggPrivateSubnet2' },
1232
1407
  ];
1233
1408
 
1409
+ // Map created subnets to discoveredResources for Aurora to use
1410
+ discoveredResources.publicSubnetId1 = { Ref: 'FriggPublicSubnet' };
1411
+ discoveredResources.publicSubnetId2 = { Ref: 'FriggPublicSubnet2' };
1412
+ discoveredResources.privateSubnetId1 = { Ref: 'FriggPrivateSubnet1' };
1413
+ discoveredResources.privateSubnetId2 = { Ref: 'FriggPrivateSubnet2' };
1414
+
1234
1415
  if (
1235
1416
  !AppDefinition.vpc.natGateway ||
1236
1417
  AppDefinition.vpc.natGateway.management === 'discover'
@@ -1293,13 +1474,22 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1293
1474
  };
1294
1475
 
1295
1476
  definition.resources.Resources.FriggPublicSubnetRouteTableAssociation =
1296
- {
1297
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1298
- Properties: {
1299
- SubnetId: { Ref: 'FriggPublicSubnet' },
1300
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
1301
- },
1302
- };
1477
+ {
1478
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1479
+ Properties: {
1480
+ SubnetId: { Ref: 'FriggPublicSubnet' },
1481
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
1482
+ },
1483
+ };
1484
+
1485
+ definition.resources.Resources.FriggPublicSubnet2RouteTableAssociation =
1486
+ {
1487
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1488
+ Properties: {
1489
+ SubnetId: { Ref: 'FriggPublicSubnet2' },
1490
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
1491
+ },
1492
+ };
1303
1493
 
1304
1494
  definition.resources.Resources.FriggLambdaRouteTable = {
1305
1495
  Type: 'AWS::EC2::RouteTable',
@@ -1316,22 +1506,22 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1316
1506
  };
1317
1507
 
1318
1508
  definition.resources.Resources.FriggPrivateSubnet1RouteTableAssociation =
1319
- {
1320
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1321
- Properties: {
1322
- SubnetId: { Ref: 'FriggPrivateSubnet1' },
1323
- RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1324
- },
1325
- };
1509
+ {
1510
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1511
+ Properties: {
1512
+ SubnetId: { Ref: 'FriggPrivateSubnet1' },
1513
+ RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1514
+ },
1515
+ };
1326
1516
 
1327
1517
  definition.resources.Resources.FriggPrivateSubnet2RouteTableAssociation =
1328
- {
1329
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1330
- Properties: {
1331
- SubnetId: { Ref: 'FriggPrivateSubnet2' },
1332
- RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1333
- },
1334
- };
1518
+ {
1519
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1520
+ Properties: {
1521
+ SubnetId: { Ref: 'FriggPrivateSubnet2' },
1522
+ RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1523
+ },
1524
+ };
1335
1525
  }
1336
1526
  } else if (subnetManagement === 'use-existing') {
1337
1527
  if (
@@ -1348,12 +1538,12 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1348
1538
  AppDefinition.vpc.subnets?.ids?.length > 0
1349
1539
  ? AppDefinition.vpc.subnets.ids
1350
1540
  : discoveredResources.privateSubnetId1 &&
1351
- discoveredResources.privateSubnetId2
1352
- ? [
1353
- discoveredResources.privateSubnetId1,
1354
- discoveredResources.privateSubnetId2,
1355
- ]
1356
- : [];
1541
+ discoveredResources.privateSubnetId2
1542
+ ? [
1543
+ discoveredResources.privateSubnetId1,
1544
+ discoveredResources.privateSubnetId2,
1545
+ ]
1546
+ : [];
1357
1547
 
1358
1548
  if (vpcConfig.subnetIds.length < 2) {
1359
1549
  if (AppDefinition.vpc.selfHeal) {
@@ -1524,7 +1714,28 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1524
1714
  Tags: [
1525
1715
  {
1526
1716
  Key: 'Name',
1527
- Value: '${self:service}-${self:provider.stage}-public-subnet',
1717
+ Value: '${self:service}-${self:provider.stage}-public-subnet-1',
1718
+ },
1719
+ { Key: 'Type', Value: 'Public' },
1720
+ ],
1721
+ },
1722
+ };
1723
+
1724
+ definition.resources.Resources.FriggPublicSubnet2 = {
1725
+ Type: 'AWS::EC2::Subnet',
1726
+ Properties: {
1727
+ VpcId: discoveredResources.defaultVpcId,
1728
+ CidrBlock:
1729
+ AppDefinition.vpc.natGateway
1730
+ ?.publicSubnetCidr2 || '172.31.251.0/24',
1731
+ AvailabilityZone: {
1732
+ 'Fn::Select': [1, { 'Fn::GetAZs': '' }],
1733
+ },
1734
+ MapPublicIpOnLaunch: true,
1735
+ Tags: [
1736
+ {
1737
+ Key: 'Name',
1738
+ Value: '${self:service}-${self:provider.stage}-public-subnet-2',
1528
1739
  },
1529
1740
  { Key: 'Type', Value: 'Public' },
1530
1741
  ],
@@ -1560,13 +1771,26 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1560
1771
  };
1561
1772
 
1562
1773
  definition.resources.Resources.FriggPublicSubnetRouteTableAssociation =
1563
- {
1564
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1565
- Properties: {
1566
- SubnetId: { Ref: 'FriggPublicSubnet' },
1567
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
1568
- },
1569
- };
1774
+ {
1775
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1776
+ Properties: {
1777
+ SubnetId: { Ref: 'FriggPublicSubnet' },
1778
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
1779
+ },
1780
+ };
1781
+
1782
+ definition.resources.Resources.FriggPublicSubnet2RouteTableAssociation =
1783
+ {
1784
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1785
+ Properties: {
1786
+ SubnetId: { Ref: 'FriggPublicSubnet2' },
1787
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
1788
+ },
1789
+ };
1790
+
1791
+ // Map created public subnets to discoveredResources for Aurora
1792
+ discoveredResources.publicSubnetId1 = { Ref: 'FriggPublicSubnet' };
1793
+ discoveredResources.publicSubnetId2 = { Ref: 'FriggPublicSubnet2' };
1570
1794
  }
1571
1795
 
1572
1796
  definition.resources.Resources.FriggNATGateway = {
@@ -1577,11 +1801,11 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1577
1801
  AllocationId: useExistingEip
1578
1802
  ? discoveredResources.existingElasticIpAllocationId
1579
1803
  : {
1580
- 'Fn::GetAtt': [
1581
- 'FriggNATGatewayEIP',
1582
- 'AllocationId',
1583
- ],
1584
- },
1804
+ 'Fn::GetAtt': [
1805
+ 'FriggNATGatewayEIP',
1806
+ 'AllocationId',
1807
+ ],
1808
+ },
1585
1809
  SubnetId: discoveredResources.publicSubnetId || {
1586
1810
  Ref: 'FriggPublicSubnet',
1587
1811
  },
@@ -1894,7 +2118,9 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1894
2118
  },
1895
2119
  };
1896
2120
 
1897
- if (AppDefinition.secretsManager?.enable === true) {
2121
+ // Create Secrets Manager VPC Endpoint if explicitly enabled OR if Aurora is enabled
2122
+ // (Aurora requires Secrets Manager access for credential retrieval)
2123
+ if (AppDefinition.secretsManager?.enable === true || AppDefinition.database?.postgres?.enable === true) {
1898
2124
  definition.resources.Resources.VPCEndpointSecretsManager = {
1899
2125
  Type: 'AWS::EC2::VPCEndpoint',
1900
2126
  Properties: {
@@ -1912,6 +2138,440 @@ const configureVpc = (definition, AppDefinition, discoveredResources) => {
1912
2138
  }
1913
2139
  };
1914
2140
 
2141
+ const createAuroraInfrastructure = (definition, AppDefinition, discoveredResources) => {
2142
+ const dbConfig = AppDefinition.database.postgres;
2143
+ const publiclyAccessible = dbConfig.publiclyAccessible === true;
2144
+
2145
+ console.log('🔧 Creating Aurora Serverless v2 infrastructure...');
2146
+ console.log(` Publicly Accessible: ${publiclyAccessible}`);
2147
+
2148
+ // 1. DB Subnet Group
2149
+ // Use public subnets if publicly accessible, private subnets otherwise
2150
+ let subnetIds;
2151
+ if (publiclyAccessible) {
2152
+ subnetIds = [discoveredResources.publicSubnetId1, discoveredResources.publicSubnetId2];
2153
+ console.log(` Using public subnets: ${subnetIds.join(', ')}`);
2154
+
2155
+ // Safety check - this should have been caught earlier, but double-check
2156
+ if (!subnetIds[0] || !subnetIds[1]) {
2157
+ throw new Error(
2158
+ 'Public subnets are required for publicly accessible Aurora deployment but were not found. ' +
2159
+ 'This should have been caught earlier in validation.'
2160
+ );
2161
+ }
2162
+ } else {
2163
+ subnetIds = [discoveredResources.privateSubnetId1, discoveredResources.privateSubnetId2];
2164
+ console.log(` Using private subnets: ${subnetIds.join(', ')}`);
2165
+
2166
+ // Safety check - this should have been caught earlier, but double-check
2167
+ if (!subnetIds[0] || !subnetIds[1]) {
2168
+ throw new Error(
2169
+ 'Private subnets are required for private Aurora deployment but were not found. ' +
2170
+ 'This should have been caught earlier in validation.'
2171
+ );
2172
+ }
2173
+ }
2174
+
2175
+ definition.resources.Resources.FriggDBSubnetGroup = {
2176
+ Type: 'AWS::RDS::DBSubnetGroup',
2177
+ Properties: {
2178
+ DBSubnetGroupDescription: `Subnet group for Frigg Aurora cluster (${publiclyAccessible ? 'public' : 'private'})`,
2179
+ SubnetIds: subnetIds,
2180
+ Tags: [
2181
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-db-subnet-group' },
2182
+ { Key: 'ManagedBy', Value: 'Frigg' },
2183
+ { Key: 'Service', Value: '${self:service}' },
2184
+ { Key: 'Stage', Value: '${self:provider.stage}' },
2185
+ ]
2186
+ }
2187
+ };
2188
+
2189
+ // 2. Security Group
2190
+ // Build security group ingress rules based on configuration
2191
+ const securityGroupIngress = [];
2192
+
2193
+ // Always allow Lambda functions to access the database (if VPC is configured)
2194
+ if (AppDefinition.vpc?.enable) {
2195
+ const lambdaSecurityGroupId = AppDefinition.vpc?.management === 'create-new'
2196
+ ? { Ref: 'FriggLambdaSecurityGroup' }
2197
+ : discoveredResources.defaultSecurityGroupId;
2198
+
2199
+ securityGroupIngress.push({
2200
+ IpProtocol: 'tcp',
2201
+ FromPort: 5432,
2202
+ ToPort: 5432,
2203
+ SourceSecurityGroupId: lambdaSecurityGroupId,
2204
+ Description: 'PostgreSQL access from Lambda functions'
2205
+ });
2206
+ }
2207
+
2208
+ // Add IP whitelist rules for public access
2209
+ if (publiclyAccessible && dbConfig.allowedIpAddresses) {
2210
+ const allowedIps = Array.isArray(dbConfig.allowedIpAddresses)
2211
+ ? dbConfig.allowedIpAddresses
2212
+ : [dbConfig.allowedIpAddresses];
2213
+
2214
+ console.log(` Adding ${allowedIps.length} whitelisted IP address(es)`);
2215
+
2216
+ allowedIps.forEach((ip, index) => {
2217
+ // Ensure IP has CIDR notation
2218
+ const cidrIp = ip.includes('/') ? ip : `${ip}/32`;
2219
+ securityGroupIngress.push({
2220
+ IpProtocol: 'tcp',
2221
+ FromPort: 5432,
2222
+ ToPort: 5432,
2223
+ CidrIp: cidrIp,
2224
+ Description: `PostgreSQL access from whitelisted IP ${index + 1}`
2225
+ });
2226
+ });
2227
+ }
2228
+
2229
+ // If publicly accessible but no IPs specified, warn the user
2230
+ if (publiclyAccessible && !dbConfig.allowedIpAddresses) {
2231
+ console.log(' ⚠️ WARNING: Database is publicly accessible but no IP whitelist configured!');
2232
+ console.log(' ⚠️ Add allowedIpAddresses to your database.postgres config for security.');
2233
+ }
2234
+
2235
+ definition.resources.Resources.FriggAuroraSecurityGroup = {
2236
+ Type: 'AWS::EC2::SecurityGroup',
2237
+ Properties: {
2238
+ GroupDescription: `Security group for Frigg Aurora PostgreSQL (${publiclyAccessible ? 'public' : 'private'})`,
2239
+ VpcId: discoveredResources.defaultVpcId,
2240
+ SecurityGroupIngress: securityGroupIngress,
2241
+ Tags: [
2242
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-sg' },
2243
+ { Key: 'ManagedBy', Value: 'Frigg' },
2244
+ { Key: 'Service', Value: '${self:service}' },
2245
+ { Key: 'Stage', Value: '${self:provider.stage}' },
2246
+ ]
2247
+ }
2248
+ };
2249
+
2250
+ // 3. Secrets Manager Secret (database credentials)
2251
+ definition.resources.Resources.FriggDatabaseSecret = {
2252
+ Type: 'AWS::SecretsManager::Secret',
2253
+ Properties: {
2254
+ Name: '${self:service}-${self:provider.stage}-aurora-credentials',
2255
+ Description: 'Aurora PostgreSQL credentials for Frigg application',
2256
+ GenerateSecretString: {
2257
+ SecretStringTemplate: JSON.stringify({
2258
+ username: dbConfig.masterUsername || 'frigg_admin'
2259
+ }),
2260
+ GenerateStringKey: 'password',
2261
+ PasswordLength: 32,
2262
+ ExcludeCharacters: '"@/\\'
2263
+ },
2264
+ Tags: [
2265
+ { Key: 'ManagedBy', Value: 'Frigg' },
2266
+ { Key: 'Service', Value: '${self:service}' },
2267
+ { Key: 'Stage', Value: '${self:provider.stage}' },
2268
+ ]
2269
+ }
2270
+ };
2271
+
2272
+ // 4. Aurora Serverless v2 Cluster
2273
+ definition.resources.Resources.FriggAuroraCluster = {
2274
+ Type: 'AWS::RDS::DBCluster',
2275
+ DeletionPolicy: 'Snapshot',
2276
+ UpdateReplacePolicy: 'Snapshot',
2277
+ Properties: {
2278
+ Engine: 'aurora-postgresql',
2279
+ EngineVersion: dbConfig.engineVersion || '15.3',
2280
+ EngineMode: 'provisioned', // Required for Serverless v2
2281
+ DatabaseName: dbConfig.databaseName || 'frigg_db',
2282
+ MasterUsername: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:username}}' },
2283
+ MasterUserPassword: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:password}}' },
2284
+ DBSubnetGroupName: { Ref: 'FriggDBSubnetGroup' },
2285
+ VpcSecurityGroupIds: [{ Ref: 'FriggAuroraSecurityGroup' }],
2286
+ ServerlessV2ScalingConfiguration: {
2287
+ MinCapacity: dbConfig.scaling?.minCapacity || 0.5,
2288
+ MaxCapacity: dbConfig.scaling?.maxCapacity || 1.0
2289
+ },
2290
+ BackupRetentionPeriod: dbConfig.backupRetentionDays || 7,
2291
+ PreferredBackupWindow: dbConfig.preferredBackupWindow || '03:00-04:00',
2292
+ DeletionProtection: dbConfig.deletionProtection !== false,
2293
+ EnableCloudwatchLogsExports: ['postgresql'],
2294
+ Tags: [
2295
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-cluster' },
2296
+ { Key: 'ManagedBy', Value: 'Frigg' },
2297
+ { Key: 'Service', Value: '${self:service}' },
2298
+ { Key: 'Stage', Value: '${self:provider.stage}' },
2299
+ ]
2300
+ }
2301
+ };
2302
+
2303
+ // 5. Aurora Serverless v2 Instance
2304
+ definition.resources.Resources.FriggAuroraInstance = {
2305
+ Type: 'AWS::RDS::DBInstance',
2306
+ Properties: {
2307
+ Engine: 'aurora-postgresql',
2308
+ DBInstanceClass: 'db.serverless',
2309
+ DBClusterIdentifier: { Ref: 'FriggAuroraCluster' },
2310
+ PubliclyAccessible: publiclyAccessible,
2311
+ EnablePerformanceInsights: dbConfig.enablePerformanceInsights || false,
2312
+ Tags: [
2313
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-aurora-instance' },
2314
+ { Key: 'ManagedBy', Value: 'Frigg' },
2315
+ { Key: 'Service', Value: '${self:service}' },
2316
+ { Key: 'Stage', Value: '${self:provider.stage}' },
2317
+ ]
2318
+ }
2319
+ };
2320
+
2321
+ // 6. Secret Attachment (links cluster to secret)
2322
+ definition.resources.Resources.FriggSecretAttachment = {
2323
+ Type: 'AWS::SecretsManager::SecretTargetAttachment',
2324
+ Properties: {
2325
+ SecretId: { Ref: 'FriggDatabaseSecret' },
2326
+ TargetId: { Ref: 'FriggAuroraCluster' },
2327
+ TargetType: 'AWS::RDS::DBCluster'
2328
+ }
2329
+ };
2330
+
2331
+ // 7. Add IAM permissions for Secrets Manager
2332
+ definition.provider.iamRoleStatements.push({
2333
+ Effect: 'Allow',
2334
+ Action: [
2335
+ 'secretsmanager:GetSecretValue',
2336
+ 'secretsmanager:DescribeSecret'
2337
+ ],
2338
+ Resource: { Ref: 'FriggDatabaseSecret' }
2339
+ });
2340
+
2341
+ // 8. Set DATABASE_URL environment variable
2342
+ definition.provider.environment.DATABASE_URL = {
2343
+ 'Fn::Sub': [
2344
+ 'postgresql://${Username}:${Password}@${Endpoint}:5432/${DatabaseName}',
2345
+ {
2346
+ Username: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:username}}' },
2347
+ Password: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:password}}' },
2348
+ Endpoint: { 'Fn::GetAtt': ['FriggAuroraCluster', 'Endpoint'] },
2349
+ DatabaseName: dbConfig.databaseName || 'frigg_db'
2350
+ }
2351
+ ]
2352
+ };
2353
+
2354
+ // 9. Set DB_TYPE for Prisma client selection
2355
+ definition.provider.environment.DB_TYPE = 'postgresql';
2356
+
2357
+ console.log('✅ Aurora infrastructure resources created');
2358
+ };
2359
+
2360
+ const useExistingAurora = (definition, AppDefinition, discoveredResources) => {
2361
+ const dbConfig = AppDefinition.database.postgres;
2362
+ const selfHeal = AppDefinition.database?.postgres?.selfHeal !== false; // Default to true
2363
+
2364
+ console.log(`🔗 Using existing Aurora cluster: ${discoveredResources.aurora.clusterIdentifier}`);
2365
+ console.log(`[DEBUG] discoveredResources.aurora.isFriggManaged: ${discoveredResources.aurora.isFriggManaged}`);
2366
+ console.log(`[DEBUG] selfHeal: ${selfHeal}`);
2367
+ console.log(`[DEBUG] discoveredResources.aurora.secretArn: ${discoveredResources.aurora.secretArn}`);
2368
+ console.log(`[DEBUG] dbConfig.secretArn: ${dbConfig.secretArn}`);
2369
+
2370
+ // Add IAM permissions for Secrets Manager if secret exists
2371
+ if (discoveredResources.aurora.secretArn) {
2372
+ definition.provider.iamRoleStatements.push({
2373
+ Effect: 'Allow',
2374
+ Action: [
2375
+ 'secretsmanager:GetSecretValue',
2376
+ 'secretsmanager:DescribeSecret'
2377
+ ],
2378
+ Resource: discoveredResources.aurora.secretArn
2379
+ });
2380
+
2381
+ // Set DATABASE_URL from discovered secret
2382
+ definition.provider.environment.DATABASE_URL = {
2383
+ 'Fn::Sub': [
2384
+ 'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
2385
+ {
2386
+ Username: { 'Fn::Sub': `{{resolve:secretsmanager:${discoveredResources.aurora.secretArn}:SecretString:username}}` },
2387
+ Password: { 'Fn::Sub': `{{resolve:secretsmanager:${discoveredResources.aurora.secretArn}:SecretString:password}}` },
2388
+ Endpoint: discoveredResources.aurora.endpoint,
2389
+ Port: discoveredResources.aurora.port,
2390
+ DatabaseName: dbConfig.databaseName || 'frigg_db'
2391
+ }
2392
+ ]
2393
+ };
2394
+ } else if (dbConfig.secretArn) {
2395
+ // Use user-provided secret ARN
2396
+ definition.provider.iamRoleStatements.push({
2397
+ Effect: 'Allow',
2398
+ Action: [
2399
+ 'secretsmanager:GetSecretValue',
2400
+ 'secretsmanager:DescribeSecret'
2401
+ ],
2402
+ Resource: dbConfig.secretArn
2403
+ });
2404
+
2405
+ definition.provider.environment.DATABASE_URL = {
2406
+ 'Fn::Sub': [
2407
+ 'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
2408
+ {
2409
+ Username: { 'Fn::Sub': `{{resolve:secretsmanager:${dbConfig.secretArn}:SecretString:username}}` },
2410
+ Password: { 'Fn::Sub': `{{resolve:secretsmanager:${dbConfig.secretArn}:SecretString:password}}` },
2411
+ Endpoint: discoveredResources.aurora.endpoint,
2412
+ Port: discoveredResources.aurora.port,
2413
+ DatabaseName: dbConfig.databaseName || 'frigg_db'
2414
+ }
2415
+ ]
2416
+ };
2417
+ } else if (selfHeal && discoveredResources.aurora?.isFriggManaged) {
2418
+ // Self-healing mode: recreate missing secret for Frigg-managed cluster
2419
+ console.log('⚠️ No database secret found for Frigg-managed cluster');
2420
+ console.log('🔧 Self-healing enabled: Creating new database secret with automatic password rotation');
2421
+
2422
+ // Get the current master username from the cluster
2423
+ const currentUsername = discoveredResources.aurora.masterUsername || dbConfig.masterUsername || 'frigg_admin';
2424
+
2425
+ // Create Secrets Manager Secret (database credentials)
2426
+ // Note: We generate a NEW password, which will be synced to the cluster via SecretTargetAttachment
2427
+ definition.resources.Resources.FriggDatabaseSecret = {
2428
+ Type: 'AWS::SecretsManager::Secret',
2429
+ Properties: {
2430
+ Name: '${self:service}-${self:provider.stage}-aurora-credentials',
2431
+ Description: 'Aurora PostgreSQL credentials for Frigg application (auto-healed)',
2432
+ GenerateSecretString: {
2433
+ SecretStringTemplate: JSON.stringify({
2434
+ username: currentUsername
2435
+ }),
2436
+ GenerateStringKey: 'password',
2437
+ PasswordLength: 32,
2438
+ ExcludeCharacters: '"@/\\`\''
2439
+ },
2440
+ Tags: [
2441
+ { Key: 'ManagedBy', Value: 'Frigg' },
2442
+ { Key: 'Service', Value: '${self:service}' },
2443
+ { Key: 'Stage', Value: '${self:provider.stage}' },
2444
+ { Key: 'AutoHealed', Value: 'true' }
2445
+ ]
2446
+ }
2447
+ };
2448
+
2449
+ // Create SecretTargetAttachment to link secret to existing cluster
2450
+ // This will automatically rotate the cluster password to match the secret!
2451
+ definition.resources.Resources.FriggSecretAttachment = {
2452
+ Type: 'AWS::SecretsManager::SecretTargetAttachment',
2453
+ Properties: {
2454
+ SecretId: { Ref: 'FriggDatabaseSecret' },
2455
+ TargetId: discoveredResources.aurora.clusterIdentifier,
2456
+ TargetType: 'AWS::RDS::DBCluster'
2457
+ }
2458
+ };
2459
+
2460
+ // Add IAM permissions for the new secret
2461
+ definition.provider.iamRoleStatements.push({
2462
+ Effect: 'Allow',
2463
+ Action: [
2464
+ 'secretsmanager:GetSecretValue',
2465
+ 'secretsmanager:DescribeSecret'
2466
+ ],
2467
+ Resource: { Ref: 'FriggDatabaseSecret' }
2468
+ });
2469
+
2470
+ // Set DATABASE_URL from new secret
2471
+ definition.provider.environment.DATABASE_URL = {
2472
+ 'Fn::Sub': [
2473
+ 'postgresql://${Username}:${Password}@${Endpoint}:${Port}/${DatabaseName}',
2474
+ {
2475
+ Username: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:username}}' },
2476
+ Password: { 'Fn::Sub': '{{resolve:secretsmanager:${FriggDatabaseSecret}:SecretString:password}}' },
2477
+ Endpoint: discoveredResources.aurora.endpoint,
2478
+ Port: discoveredResources.aurora.port,
2479
+ DatabaseName: dbConfig.databaseName || 'frigg_db'
2480
+ }
2481
+ ]
2482
+ };
2483
+
2484
+ console.log('✅ Self-healing configuration complete:');
2485
+ console.log(' - New secret will be created with auto-generated password');
2486
+ console.log(' - SecretTargetAttachment will automatically update cluster password');
2487
+ console.log(' - No manual password sync required!');
2488
+ } else {
2489
+ throw new Error(
2490
+ 'No database secret found. Options:\n' +
2491
+ ' 1. Provide secretArn in database.postgres configuration\n' +
2492
+ ' 2. Ensure Secrets Manager secret exists\n' +
2493
+ ' 3. Enable self-healing: set database.postgres.selfHeal to true (for Frigg-managed clusters only)'
2494
+ );
2495
+ }
2496
+
2497
+ // Set DB_TYPE for Prisma client selection
2498
+ definition.provider.environment.DB_TYPE = 'postgresql';
2499
+
2500
+ console.log('✅ Existing Aurora cluster configured');
2501
+ };
2502
+
2503
+ const useDiscoveredAurora = (definition, AppDefinition, discoveredResources) => {
2504
+ console.log(`🔍 Using discovered Aurora cluster: ${discoveredResources.aurora.clusterIdentifier}`);
2505
+ useExistingAurora(definition, AppDefinition, discoveredResources);
2506
+ };
2507
+
2508
+ const configurePostgres = (definition, AppDefinition, discoveredResources) => {
2509
+ if (!AppDefinition.database?.postgres?.enable) {
2510
+ return;
2511
+ }
2512
+
2513
+ const dbConfig = AppDefinition.database.postgres;
2514
+ const publiclyAccessible = dbConfig.publiclyAccessible === true;
2515
+
2516
+ // Validate VPC is enabled for private deployments
2517
+ // Public deployments can work without VPC if using default VPC
2518
+ if (!publiclyAccessible && !AppDefinition.vpc?.enable) {
2519
+ throw new Error(
2520
+ 'Aurora PostgreSQL requires VPC deployment for private access. ' +
2521
+ 'Either set vpc.enable to true, or set database.postgres.publiclyAccessible to true for public access.'
2522
+ );
2523
+ }
2524
+
2525
+ // Validate subnets based on deployment type
2526
+ const vpcManagement = AppDefinition.vpc?.management || 'discover';
2527
+
2528
+ if (publiclyAccessible) {
2529
+ // For public deployments, validate public subnets exist
2530
+ if (vpcManagement !== 'create-new' && (!discoveredResources.publicSubnetId1 || !discoveredResources.publicSubnetId2)) {
2531
+ throw new Error(
2532
+ 'Aurora PostgreSQL with publiclyAccessible requires at least 2 public subnets in different availability zones. ' +
2533
+ 'No public subnets were discovered in your VPC. ' +
2534
+ 'Options:\n' +
2535
+ ' 1. Create public subnets in your VPC with Internet Gateway attached\n' +
2536
+ ' 2. Use VPC management mode "create-new" (will create public subnets automatically)\n' +
2537
+ ' 3. Set publiclyAccessible to false and use private subnets instead'
2538
+ );
2539
+ }
2540
+ } else {
2541
+ // For private deployments, validate private subnets exist
2542
+ if (vpcManagement !== 'create-new' && (!discoveredResources.privateSubnetId1 || !discoveredResources.privateSubnetId2)) {
2543
+ throw new Error(
2544
+ 'Aurora PostgreSQL requires at least 2 private subnets in different availability zones for private deployment. ' +
2545
+ 'No private subnets were discovered in your VPC. ' +
2546
+ 'Options:\n' +
2547
+ ' 1. Create private subnets in your VPC\n' +
2548
+ ' 2. Use VPC management mode "create-new" (will create private subnets automatically)\n' +
2549
+ ' 3. Set publiclyAccessible to true and use public subnets instead'
2550
+ );
2551
+ }
2552
+ }
2553
+
2554
+ const management = dbConfig.management || 'discover';
2555
+
2556
+ console.log(`\n🐘 PostgreSQL Management Mode: ${management}`);
2557
+
2558
+ if (management === 'create-new' || discoveredResources.aurora?.needsCreation) {
2559
+ createAuroraInfrastructure(definition, AppDefinition, discoveredResources);
2560
+ } else if (management === 'use-existing') {
2561
+ if (!discoveredResources.aurora?.clusterIdentifier && !dbConfig.clusterIdentifier) {
2562
+ throw new Error('PostgreSQL management is set to "use-existing" but no clusterIdentifier was found or provided');
2563
+ }
2564
+ useExistingAurora(definition, AppDefinition, discoveredResources);
2565
+ } else {
2566
+ // discover mode
2567
+ if (discoveredResources.aurora?.clusterIdentifier) {
2568
+ useDiscoveredAurora(definition, AppDefinition, discoveredResources);
2569
+ } else {
2570
+ throw new Error('No Aurora cluster found in discovery mode. Set management to "create-new" or provide clusterIdentifier with "use-existing".');
2571
+ }
2572
+ }
2573
+ };
2574
+
1915
2575
  const configureSsm = (definition, AppDefinition) => {
1916
2576
  if (AppDefinition.ssm?.enable !== true) {
1917
2577
  return;
@@ -1949,19 +2609,31 @@ const attachIntegrations = (definition, AppDefinition) => {
1949
2609
  `Processing ${AppDefinition.integrations.length} integrations...`
1950
2610
  );
1951
2611
 
2612
+ // Get the functionPackageConfig from the definition (defined in createBaseDefinition)
2613
+ const functionPackageConfig = {
2614
+ exclude: [
2615
+ 'node_modules/aws-sdk/**',
2616
+ 'node_modules/@aws-sdk/**',
2617
+ 'node_modules/@prisma/**',
2618
+ 'node_modules/.prisma/**',
2619
+ 'node_modules/prisma/**',
2620
+ 'node_modules/@friggframework/core/generated/**',
2621
+ ],
2622
+ };
2623
+
1952
2624
  for (const integration of AppDefinition.integrations) {
1953
2625
  if (!integration?.Definition?.name) {
1954
2626
  throw new Error('Invalid integration: missing Definition or name');
1955
2627
  }
1956
2628
 
1957
2629
  const integrationName = integration.Definition.name;
1958
- const queueReference = `${
1959
- integrationName.charAt(0).toUpperCase() + integrationName.slice(1)
1960
- }Queue`;
2630
+ const queueReference = `${integrationName.charAt(0).toUpperCase() + integrationName.slice(1)
2631
+ }Queue`;
1961
2632
  const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
1962
2633
 
1963
2634
  definition.functions[integrationName] = {
1964
2635
  handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
2636
+ package: functionPackageConfig,
1965
2637
  events: [
1966
2638
  {
1967
2639
  httpApi: {
@@ -1990,6 +2662,7 @@ const attachIntegrations = (definition, AppDefinition) => {
1990
2662
  const queueWorkerName = `${integrationName}QueueWorker`;
1991
2663
  definition.functions[queueWorkerName] = {
1992
2664
  handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
2665
+ package: functionPackageConfig,
1993
2666
  reservedConcurrency: 5,
1994
2667
  events: [
1995
2668
  {
@@ -2013,10 +2686,7 @@ const attachIntegrations = (definition, AppDefinition) => {
2013
2686
 
2014
2687
  // Add webhook handler if enabled
2015
2688
  const webhookConfig = integration.Definition.webhooks;
2016
- if (
2017
- webhookConfig &&
2018
- (webhookConfig === true || webhookConfig.enabled === true)
2019
- ) {
2689
+ if (webhookConfig && (webhookConfig === true || webhookConfig.enabled === true)) {
2020
2690
  const webhookFunctionName = `${integrationName}Webhook`;
2021
2691
 
2022
2692
  definition.functions[webhookFunctionName] = {
@@ -2056,9 +2726,44 @@ const configureWebsockets = (definition, AppDefinition) => {
2056
2726
  };
2057
2727
  };
2058
2728
 
2729
+ /**
2730
+ * Ensure Prisma Lambda Layer exists
2731
+ * Automatically builds the layer if it doesn't exist in the project root
2732
+ * @param {Object} databaseConfig - Database configuration from AppDefinition.database
2733
+ */
2734
+ async function ensurePrismaLayerExists(databaseConfig = {}) {
2735
+ const projectRoot = process.cwd();
2736
+ const layerPath = path.join(projectRoot, 'layers/prisma');
2737
+
2738
+ // Check if layer already exists
2739
+ if (fs.existsSync(layerPath)) {
2740
+ console.log('✓ Prisma Lambda Layer already exists at', layerPath);
2741
+ return;
2742
+ }
2743
+
2744
+ // Layer doesn't exist - build it automatically
2745
+ console.log('📦 Prisma Lambda Layer not found - building automatically...');
2746
+ console.log(' Building layer with CLI (used by all functions including dbMigrate)');
2747
+ console.log(' This may take a minute on first deployment.\n');
2748
+
2749
+ try {
2750
+ // Build layer WITH CLI (includeCLI = true) - all functions use same layer
2751
+ await buildPrismaLayer(databaseConfig, true);
2752
+ console.log('✓ Prisma Lambda Layer built successfully\n');
2753
+ } catch (error) {
2754
+ console.error('✗ Failed to build Prisma Lambda Layer:', error.message);
2755
+ console.error(' You may need to run: npm install @friggframework/core\n');
2756
+ throw error;
2757
+ }
2758
+ }
2759
+
2059
2760
  const composeServerlessDefinition = async (AppDefinition) => {
2060
2761
  console.log('composeServerlessDefinition', AppDefinition);
2061
2762
 
2763
+ // Ensure Prisma layer exists before generating serverless config
2764
+ // Pass database config so layer only includes needed database clients
2765
+ await ensurePrismaLayerExists(AppDefinition.database || {});
2766
+
2062
2767
  const discoveredResources = await gatherDiscoveredResources(AppDefinition);
2063
2768
  const appEnvironmentVars = getAppEnvironmentVars(AppDefinition);
2064
2769
  const definition = createBaseDefinition(
@@ -2080,6 +2785,7 @@ const composeServerlessDefinition = async (AppDefinition) => {
2080
2785
  if (!isLocalBuild) {
2081
2786
  applyKmsConfiguration(definition, AppDefinition, discoveredResources);
2082
2787
  configureVpc(definition, AppDefinition, discoveredResources);
2788
+ configurePostgres(definition, AppDefinition, discoveredResources);
2083
2789
  configureSsm(definition, AppDefinition);
2084
2790
  } else {
2085
2791
  console.log(