From 05334c99efbafedd8ef66c90de7b6b402688c8bf Mon Sep 17 00:00:00 2001 From: savchenko-vladyslav Date: Fri, 19 Sep 2025 11:40:03 +0300 Subject: [PATCH 01/18] chore: added db cluster creation on infra deployment --- infra/environments/staging/main.tf | 18 ++++++++++ infra/environments/staging/variables.tf | 24 +++++++++++++ infra/modules/app/main.tf | 45 ++++++++++++++++++++++++- infra/modules/app/variables.tf | 24 +++++++++++++ 4 files changed, 110 insertions(+), 1 deletion(-) diff --git a/infra/environments/staging/main.tf b/infra/environments/staging/main.tf index 3e129ca..6993c2b 100644 --- a/infra/environments/staging/main.tf +++ b/infra/environments/staging/main.tf @@ -52,4 +52,22 @@ module "saas_template_app" { logtail_source_name = "saas-staging-logtail" logtail_source_token = var.logtail_source_token + + # Database configuration + project_name = var.project_name + create_database = var.create_database + database_size = var.database_size + database_node_count = var.database_node_count +} + +# Output database connection info if database is created +output "database_uri" { + value = module.saas_template_app.database_uri + sensitive = true + description = "Database connection URI (if database is created)" +} + +output "app_url" { + value = module.saas_template_app.app_url + description = "The live URL of the deployed app" } diff --git a/infra/environments/staging/variables.tf b/infra/environments/staging/variables.tf index 0542dcf..f4030f2 100644 --- a/infra/environments/staging/variables.tf +++ b/infra/environments/staging/variables.tf @@ -22,3 +22,27 @@ variable "github_token" { type = string default = "autoreplace_github_token" } + +variable "project_name" { + description = "Name of the DigitalOcean project" + type = string + default = "SaaS Template" +} + +variable "create_database" { + description = "Whether to create a managed database" + type = bool + default = false +} + +variable "database_size" { + description = "Size of the database cluster" + type = string + default = "db-s-1vcpu-1gb" +} + +variable "database_node_count" { + description = "Number of nodes in the database cluster" + type = number + default = 1 +} diff --git a/infra/modules/app/main.tf b/infra/modules/app/main.tf index 6e54b4c..5a7cc0f 100644 --- a/infra/modules/app/main.tf +++ b/infra/modules/app/main.tf @@ -14,7 +14,7 @@ provider "digitalocean" { resource "digitalocean_project" "saas_template" { - name = "SaaS Template" + name = var.project_name description = "A project for the SaaS Template application" purpose = "Web Application" environment = "Development" @@ -131,3 +131,46 @@ resource "digitalocean_app" "saas_template" { } } } + +# Optional managed database +resource "digitalocean_database_cluster" "postgres" { + count = var.create_database ? 1 : 0 + name = "${var.name}-db" + engine = "pg" + version = "15" + size = var.database_size + region = var.region + node_count = var.database_node_count + + project_id = digitalocean_project.saas_template.id +} + +resource "digitalocean_database_db" "app_database" { + count = var.create_database ? 1 : 0 + cluster_id = digitalocean_database_cluster.postgres[0].id + name = "app" +} + +resource "digitalocean_database_user" "app_user" { + count = var.create_database ? 1 : 0 + cluster_id = digitalocean_database_cluster.postgres[0].id + name = "appuser" +} + +# Outputs for database connection info +output "database_uri" { + value = var.create_database ? digitalocean_database_cluster.postgres[0].uri : "" + sensitive = true + description = "Database connection URI" +} + +output "database_private_uri" { + value = var.create_database ? digitalocean_database_cluster.postgres[0].private_uri : "" + sensitive = true + description = "Private database connection URI" +} + +output "app_url" { + value = digitalocean_app.saas_template.live_url + description = "The live URL of the deployed app" +} diff --git a/infra/modules/app/variables.tf b/infra/modules/app/variables.tf index 9e6cf16..f0ce3a2 100644 --- a/infra/modules/app/variables.tf +++ b/infra/modules/app/variables.tf @@ -111,3 +111,27 @@ variable "env_vars" { value = string })) } + +variable "project_name" { + description = "Name of the DigitalOcean project" + type = string + default = "SaaS Template" +} + +variable "create_database" { + description = "Whether to create a managed database" + type = bool + default = false +} + +variable "database_size" { + description = "Size of the database cluster" + type = string + default = "db-s-1vcpu-1gb" +} + +variable "database_node_count" { + description = "Number of nodes in the database cluster" + type = number + default = 1 +} From a0ddf56dd53b2fe4c9ca005b58bae44991ec7272 Mon Sep 17 00:00:00 2001 From: "vladyslav.savchenko@softcery.com" Date: Fri, 19 Sep 2025 17:07:12 +0300 Subject: [PATCH 02/18] chore: added ssl configuration in db module --- api/src/shared/infrastructure/database/database.module.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/api/src/shared/infrastructure/database/database.module.ts b/api/src/shared/infrastructure/database/database.module.ts index 702197a..e1aea5f 100644 --- a/api/src/shared/infrastructure/database/database.module.ts +++ b/api/src/shared/infrastructure/database/database.module.ts @@ -10,7 +10,13 @@ import { mergeDbdSchema } from './drizzle/schema/merged-schema'; export const drizzlePostgresModule = DrizzlePostgresModule.registerAsync({ useFactory: (appConfig: IAppConfigService) => ({ - db: { config: { connectionString: appConfig.get('DB_URL') }, connection: 'pool' }, + db: { + config: { + connectionString: appConfig.get('DB_URL'), + ssl: { rejectUnauthorized: false } + }, + connection: 'pool' + }, schema: mergeDbdSchema, }), inject: [BaseToken.APP_CONFIG], From 5a098ce336a71a38ae2a0f291196c423de183b55 Mon Sep 17 00:00:00 2001 From: "vladyslav.savchenko@softcery.com" Date: Sun, 21 Sep 2025 13:01:22 +0300 Subject: [PATCH 03/18] refactor(api): use separate DB connection vars instead of DB_URL --- api/config/config.staging.sh | 28 +++++++++++++++++ .../application/models/app-config.model.ts | 20 ++++++++++-- .../database/database.module.ts | 31 +++++++++++++------ 3 files changed, 67 insertions(+), 12 deletions(-) mode change 100644 => 100755 api/config/config.staging.sh diff --git a/api/config/config.staging.sh b/api/config/config.staging.sh old mode 100644 new mode 100755 index 5cc869a..430b1c0 --- a/api/config/config.staging.sh +++ b/api/config/config.staging.sh @@ -1,6 +1,34 @@ +# Database Configuration +# Note: Configure these values if database is not created via Terraform +# For Terraform-managed databases, these will be automatically populated from infrastructure outputs +DB_HOST="YOUR_DB_HOST" +DB_PORT="YOUR_DB_PORT" +DB_USER="YOUR_DB_USER" +DB_PASSWORD="YOUR_DB_PASSWORD" +DB_NAME="YOUR_DB_NAME" +DB_CA="" # Add CA certificate content if needed for SSL connections + +# Supabase Configuration +SUPABASE_URL=https://your-supabase-app-name.supabase.co +SUPABASE_SECRET_KEY=YOUR_SUPABASE_SECRET_KEY_HERE + +# Google OAuth Configuration +GOOGLE_OAUTH_CLIENT_ID=YOUR_GOOGLE_OAUTH_CLIENT_ID_HERE +GOOGLE_OAUTH_SECRET=YOUR_GOOGLE_OAUTH_SECRET_HERE GOOGLE_OAUTH_CALLBACK_URL=https://stag-saas-template-46ccu.ondigitalocean.app/api/auth/oauth/google-callback + +# Stripe Configuration +STRIPE_API_KEY=YOUR_STRIPE_API_KEY_HERE +STRIPE_WEBHOOK_SIGNING_SECRET=YOUR_STRIPE_WEBHOOK_SIGNING_SECRET_HERE + +# JWT Configuration +JWT_SECRET=YOUR_JWT_SECRET_HERE + +# Application Configuration PASSWORD_RESET_REDIRECT_URL=https://stag-saas-template-46ccu.ondigitalocean.app/reset CLIENT_AUTH_REDIRECT_URL=https://stag-saas-template-46ccu.ondigitalocean.app/sign-in PASSWORD_RECOVERY_TIME=900 BILLING_SUCCESS_REDIRECT_URL=https://stag-saas-template-46ccu.ondigitalocean.app/billing TRIAL_PERIOD_DURATION_DAYS=14 + +TEST=test \ No newline at end of file diff --git a/api/src/shared/application/models/app-config.model.ts b/api/src/shared/application/models/app-config.model.ts index e7c7170..2e1cd1c 100644 --- a/api/src/shared/application/models/app-config.model.ts +++ b/api/src/shared/application/models/app-config.model.ts @@ -1,11 +1,27 @@ -import { IsInt, IsPositive, IsString } from 'class-validator'; +import { IsInt, IsOptional, IsPositive, IsString } from 'class-validator'; export class AppConfigModel { @IsString() TEST: string; @IsString() - DB_URL: string; + DB_HOST: string; + + @IsString() + DB_PORT: string; + + @IsString() + DB_USER: string; + + @IsString() + DB_PASSWORD: string; + + @IsString() + DB_NAME: string; + + @IsOptional() + @IsString() + DB_CA?: string; @IsString() SUPABASE_URL: string; diff --git a/api/src/shared/infrastructure/database/database.module.ts b/api/src/shared/infrastructure/database/database.module.ts index e1aea5f..845cc57 100644 --- a/api/src/shared/infrastructure/database/database.module.ts +++ b/api/src/shared/infrastructure/database/database.module.ts @@ -9,16 +9,27 @@ import { DrizzleDbContext } from './drizzle/db-context/drizzle-db-context'; import { mergeDbdSchema } from './drizzle/schema/merged-schema'; export const drizzlePostgresModule = DrizzlePostgresModule.registerAsync({ - useFactory: (appConfig: IAppConfigService) => ({ - db: { - config: { - connectionString: appConfig.get('DB_URL'), - ssl: { rejectUnauthorized: false } - }, - connection: 'pool' - }, - schema: mergeDbdSchema, - }), + useFactory: (appConfig: IAppConfigService) => { + const databaseCa = appConfig.get('DB_CA'); + + return { + db: { + config: { + host: appConfig.get('DB_HOST'), + port: parseInt(appConfig.get('DB_PORT')), + user: appConfig.get('DB_USER'), + password: appConfig.get('DB_PASSWORD'), + database: appConfig.get('DB_NAME'), + ssl: databaseCa ? { + rejectUnauthorized: true, + ca: databaseCa, + } : false, + }, + connection: 'pool' + }, + schema: mergeDbdSchema, + }; + }, inject: [BaseToken.APP_CONFIG], }); From 19da6c5c54c196e5fb511957e1a0020d8e0e03f4 Mon Sep 17 00:00:00 2001 From: "vladyslav.savchenko@softcery.com" Date: Sun, 21 Sep 2025 16:50:43 +0300 Subject: [PATCH 04/18] chore(api): added temo logs for db vars --- api/src/application.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/api/src/application.ts b/api/src/application.ts index 4906483..3ee45a7 100644 --- a/api/src/application.ts +++ b/api/src/application.ts @@ -18,6 +18,14 @@ export class Application { this.nodeArguments = FlagParser.getFlags(process.argv); } public async init() { + console.log('Application initialized:', { + db_name: process.env.DB_NAME, + db_host: process.env.DB_HOST, + db_password: process.env.DB_PASSWORD, + db_port: process.env.DB_PORT, + db_user: process.env.DB_USER, + db_ca: process.env.DB_CA, + }); this.app = await NestFactory.create(AppModule, { bodyParser: true, }); From 8411897e48d866510fd66b488f5c8a7d4f6f78d7 Mon Sep 17 00:00:00 2001 From: "vladyslav.savchenko@softcery.com" Date: Sun, 21 Sep 2025 17:01:34 +0300 Subject: [PATCH 05/18] chore(api): test logs for db ca --- api/src/application.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/src/application.ts b/api/src/application.ts index 3ee45a7..001e680 100644 --- a/api/src/application.ts +++ b/api/src/application.ts @@ -24,8 +24,9 @@ export class Application { db_password: process.env.DB_PASSWORD, db_port: process.env.DB_PORT, db_user: process.env.DB_USER, - db_ca: process.env.DB_CA, }); + console.log("DB CA") + console.log(process.env.DB_CA) this.app = await NestFactory.create(AppModule, { bodyParser: true, }); From 5d59fdd4a74ec47bd203bdf03a6e509f6166e717 Mon Sep 17 00:00:00 2001 From: "vladyslav.savchenko@softcery.com" Date: Mon, 22 Sep 2025 12:16:50 +0300 Subject: [PATCH 06/18] chore(infra): updated infra with database cluster, updated s3 back with some skip flags --- infra/environments/staging/main.tf | 12 ++- infra/environments/staging/variables.tf | 7 +- infra/modules/app/main.tf | 103 +++++++++++++++++++++++- infra/modules/app/variables.tf | 6 ++ 4 files changed, 121 insertions(+), 7 deletions(-) diff --git a/infra/environments/staging/main.tf b/infra/environments/staging/main.tf index 6993c2b..3f06b07 100644 --- a/infra/environments/staging/main.tf +++ b/infra/environments/staging/main.tf @@ -7,12 +7,17 @@ terraform { } backend "s3" { - endpoint = "fra1.digitaloceanspaces.com" + endpoints = { + s3 = "https://fra1.digitaloceanspaces.com" + } region = "us-west-1" bucket = "saas-template-space" key = "staging_terraform.tfstate" skip_credentials_validation = true skip_metadata_api_check = true + skip_region_validation = true + skip_s3_checksum = true + skip_requesting_account_id = true } } @@ -29,7 +34,7 @@ module "env_config" { module "saas_template_app" { source = "../../modules/app" name = "stag-saas-template" - region = "lon" + region = "fra" source_dir_static = "./web" source_dir_api = "./api" environment_slug = "node-js" @@ -38,7 +43,7 @@ module "saas_template_app" { catchall_document = "index.html" static_build_command = "pnpm run build:digitalocean" gh_repository = "softcery/saas-template" - branch = "staging" + branch = "infra/db-cluster" deploy_on_push = false do_token = var.do_token github_token = var.github_token @@ -58,6 +63,7 @@ module "saas_template_app" { create_database = var.create_database database_size = var.database_size database_node_count = var.database_node_count + database_region = var.database_region } # Output database connection info if database is created diff --git a/infra/environments/staging/variables.tf b/infra/environments/staging/variables.tf index f4030f2..fce772e 100644 --- a/infra/environments/staging/variables.tf +++ b/infra/environments/staging/variables.tf @@ -32,7 +32,7 @@ variable "project_name" { variable "create_database" { description = "Whether to create a managed database" type = bool - default = false + default = true } variable "database_size" { @@ -46,3 +46,8 @@ variable "database_node_count" { type = number default = 1 } + +variable "database_region" { + description = "Region for the database cluster" + type = string +} diff --git a/infra/modules/app/main.tf b/infra/modules/app/main.tf index 5a7cc0f..568ae1d 100644 --- a/infra/modules/app/main.tf +++ b/infra/modules/app/main.tf @@ -21,7 +21,10 @@ resource "digitalocean_project" "saas_template" { } resource "digitalocean_app" "saas_template" { - depends_on = [ digitalocean_project.saas_template ] + depends_on = [ + digitalocean_project.saas_template, + digitalocean_database_cluster.postgres + ] project_id = digitalocean_project.saas_template.id spec { name = var.name @@ -69,6 +72,60 @@ resource "digitalocean_app" "saas_template" { } } + dynamic "env" { + for_each = var.create_database ? [1] : [] + content { + key = "DB_HOST" + scope = "RUN_TIME" + value = digitalocean_database_cluster.postgres[0].host + } + } + + dynamic "env" { + for_each = var.create_database ? [1] : [] + content { + key = "DB_PORT" + scope = "RUN_TIME" + value = tostring(digitalocean_database_cluster.postgres[0].port) + } + } + + dynamic "env" { + for_each = var.create_database ? [1] : [] + content { + key = "DB_USER" + scope = "RUN_TIME" + value = digitalocean_database_user.app_user[0].name + } + } + + dynamic "env" { + for_each = var.create_database ? [1] : [] + content { + key = "DB_PASSWORD" + scope = "RUN_TIME" + value = digitalocean_database_user.app_user[0].password + } + } + + dynamic "env" { + for_each = var.create_database ? [1] : [] + content { + key = "DB_NAME" + scope = "RUN_TIME" + value = digitalocean_database_db.app_database[0].name + } + } + + dynamic "env" { + for_each = var.create_database ? [1] : [] + content { + key = "DB_CA" + scope = "RUN_TIME" + value = data.digitalocean_database_ca.postgres[0].certificate + } + } + github { repo = var.gh_repository deploy_on_push = var.deploy_on_push @@ -132,14 +189,13 @@ resource "digitalocean_app" "saas_template" { } } -# Optional managed database resource "digitalocean_database_cluster" "postgres" { count = var.create_database ? 1 : 0 name = "${var.name}-db" engine = "pg" version = "15" size = var.database_size - region = var.region + region = var.database_region node_count = var.database_node_count project_id = digitalocean_project.saas_template.id @@ -157,6 +213,11 @@ resource "digitalocean_database_user" "app_user" { name = "appuser" } +data "digitalocean_database_ca" "postgres" { + count = var.create_database ? 1 : 0 + cluster_id = digitalocean_database_cluster.postgres[0].id +} + # Outputs for database connection info output "database_uri" { value = var.create_database ? digitalocean_database_cluster.postgres[0].uri : "" @@ -170,6 +231,42 @@ output "database_private_uri" { description = "Private database connection URI" } +output "database_host" { + value = var.create_database ? digitalocean_database_cluster.postgres[0].host : "" + sensitive = true + description = "Database host" +} + +output "database_port" { + value = var.create_database ? digitalocean_database_cluster.postgres[0].port : "" + sensitive = true + description = "Database port" +} + +output "database_user" { + value = var.create_database ? digitalocean_database_user.app_user[0].name : "" + sensitive = true + description = "Database user" +} + +output "database_password" { + value = var.create_database ? digitalocean_database_user.app_user[0].password : "" + sensitive = true + description = "Database password" +} + +output "database_name" { + value = var.create_database ? digitalocean_database_db.app_database[0].name : "" + sensitive = true + description = "Database name" +} + +output "database_ca_certificate" { + value = var.create_database ? data.digitalocean_database_ca.postgres[0].certificate : "" + sensitive = true + description = "Database CA certificate" +} + output "app_url" { value = digitalocean_app.saas_template.live_url description = "The live URL of the deployed app" diff --git a/infra/modules/app/variables.tf b/infra/modules/app/variables.tf index f0ce3a2..02e3510 100644 --- a/infra/modules/app/variables.tf +++ b/infra/modules/app/variables.tf @@ -135,3 +135,9 @@ variable "database_node_count" { type = number default = 1 } + +variable "database_region" { + description = "Region for the database cluster" + type = string + default = "fra1" +} From b7e8c1a3929a3e80df869bdaa0326be9e02d7af8 Mon Sep 17 00:00:00 2001 From: "vladyslav.savchenko@softcery.com" Date: Mon, 22 Sep 2025 12:19:35 +0300 Subject: [PATCH 07/18] chore(api): updated config staging sh with database configuration vars --- api/config/config.staging.sh | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/api/config/config.staging.sh b/api/config/config.staging.sh index 430b1c0..5c21bf3 100755 --- a/api/config/config.staging.sh +++ b/api/config/config.staging.sh @@ -1,12 +1,13 @@ # Database Configuration -# Note: Configure these values if database is not created via Terraform -# For Terraform-managed databases, these will be automatically populated from infrastructure outputs -DB_HOST="YOUR_DB_HOST" -DB_PORT="YOUR_DB_PORT" -DB_USER="YOUR_DB_USER" -DB_PASSWORD="YOUR_DB_PASSWORD" -DB_NAME="YOUR_DB_NAME" -DB_CA="" # Add CA certificate content if needed for SSL connections +# IMPORTANT: Only uncomment and configure these if create_database = false in your terraform.tfvars +# If using DigitalOcean managed database (create_database = true), these are automatically provided by Terraform +# DO NOT include these variables when using Terraform-managed database +#DB_HOST="YOUR_DB_HOST" +#DB_PORT="YOUR_DB_PORT" +#DB_USER="YOUR_DB_USER" +#DB_PASSWORD="YOUR_DB_PASSWORD" +#DB_NAME="YOUR_DB_NAME" +#DB_CA="" # Add CA certificate content if needed for SSL connections # Supabase Configuration SUPABASE_URL=https://your-supabase-app-name.supabase.co From 30561e2d171777e7e07c973fcdf255bd438a82bc Mon Sep 17 00:00:00 2001 From: "vladyslav.savchenko@softcery.com" Date: Mon, 22 Sep 2025 12:20:15 +0300 Subject: [PATCH 08/18] chore: updated gh workflow with new DB secrets instead of old DB URL --- .github/workflows/release-staging.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-staging.yml b/.github/workflows/release-staging.yml index 905f629..531aadf 100644 --- a/.github/workflows/release-staging.yml +++ b/.github/workflows/release-staging.yml @@ -1,3 +1,4 @@ + name: Release – Staging on: @@ -25,7 +26,12 @@ jobs: SUPABASE_SECRET_KEY: ${{secrets.SUPABASE_SECRET_KEY}} GOOGLE_OAUTH_CLIENT_ID: ${{secrets.GOOGLE_OAUTH_CLIENT_ID}} GOOGLE_OAUTH_SECRET: ${{secrets.GOOGLE_OAUTH_SECRET}} - DB_URL: ${{secrets.DB_URL}} + DB_HOST: ${{secrets.DB_HOST}} + DB_PORT: ${{secrets.DB_PORT}} + DB_USER: ${{secrets.DB_USER}} + DB_PASSWORD: ${{secrets.DB_PASSWORD}} + DB_NAME: ${{secrets.DB_NAME}} + DB_CA: ${{secrets.DB_CA}} STRIPE_API_KEY: ${{secrets.STRIPE_API_KEY}} STRIPE_WEBHOOK_SIGNING_SECRET: ${{secrets.STRIPE_WEBHOOK_SIGNING_SECRET}} JWT_SECRET: ${{secrets.JWT_SECRET}} @@ -43,7 +49,12 @@ jobs: SUPABASE_SECRET_KEY=${{ env.SUPABASE_SECRET_KEY }} GOOGLE_OAUTH_CLIENT_ID=${{ env.GOOGLE_OAUTH_CLIENT_ID }} GOOGLE_OAUTH_SECRET=${{ env.GOOGLE_OAUTH_SECRET }} - DB_URL=${{ env.DB_URL }} + DB_HOST=${{ env.DB_HOST }} + DB_PORT=${{ env.DB_PORT }} + DB_USER=${{ env.DB_USER }} + DB_PASSWORD=${{ env.DB_PASSWORD }} + DB_NAME=${{ env.DB_NAME }} + DB_CA="${{ env.DB_CA }}" STRIPE_API_KEY=${{ env.STRIPE_API_KEY }} STRIPE_WEBHOOK_SIGNING_SECRET=${{ env.STRIPE_WEBHOOK_SIGNING_SECRET }} JWT_SECRET=${{ env.JWT_SECRET }} From 058880f3038be139ec9d72dc35ee6bd483756667 Mon Sep 17 00:00:00 2001 From: "vladyslav.savchenko@softcery.com" Date: Mon, 22 Sep 2025 13:02:35 +0300 Subject: [PATCH 09/18] test: updated gh workflow with current branch, updated variables create_database to false --- .github/workflows/release-staging.yml | 1 + infra/environments/staging/variables.tf | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-staging.yml b/.github/workflows/release-staging.yml index 531aadf..b6985f3 100644 --- a/.github/workflows/release-staging.yml +++ b/.github/workflows/release-staging.yml @@ -6,6 +6,7 @@ on: push: branches: - staging + - infra/db-cluster concurrency: group: ${{ github.head_ref || github.ref }} diff --git a/infra/environments/staging/variables.tf b/infra/environments/staging/variables.tf index fce772e..8b045dd 100644 --- a/infra/environments/staging/variables.tf +++ b/infra/environments/staging/variables.tf @@ -32,7 +32,7 @@ variable "project_name" { variable "create_database" { description = "Whether to create a managed database" type = bool - default = true + default = false } variable "database_size" { @@ -50,4 +50,5 @@ variable "database_node_count" { variable "database_region" { description = "Region for the database cluster" type = string + default = "fra1" } From fc2f91697de278f665ab740593a21ceb04005c4c Mon Sep 17 00:00:00 2001 From: "vladyslav.savchenko@softcery.com" Date: Mon, 22 Sep 2025 13:06:48 +0300 Subject: [PATCH 10/18] chore: updated terraform version in gh workflow --- .github/workflows/release-staging.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-staging.yml b/.github/workflows/release-staging.yml index b6985f3..a3354c4 100644 --- a/.github/workflows/release-staging.yml +++ b/.github/workflows/release-staging.yml @@ -21,7 +21,7 @@ jobs: timeout-minutes: 15 environment: staging env: - TF_VERSION: 1.4.6 + TF_VERSION: 1.13.3 APP_NAME: 'stag-saas-template' SUPABASE_URL: ${{secrets.SUPABASE_URL}} SUPABASE_SECRET_KEY: ${{secrets.SUPABASE_SECRET_KEY}} From 95e11fa41b7f05135b26f6aed93fdd96c81e401f Mon Sep 17 00:00:00 2001 From: "vladyslav.savchenko@softcery.com" Date: Mon, 22 Sep 2025 13:15:21 +0300 Subject: [PATCH 11/18] chore(infra): returned create_database to true --- infra/modules/app/variables.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/modules/app/variables.tf b/infra/modules/app/variables.tf index 02e3510..7134a91 100644 --- a/infra/modules/app/variables.tf +++ b/infra/modules/app/variables.tf @@ -121,7 +121,7 @@ variable "project_name" { variable "create_database" { description = "Whether to create a managed database" type = bool - default = false + default = true } variable "database_size" { From dcdfce724c5ac73faa7403875542a6985abf250f Mon Sep 17 00:00:00 2001 From: "vladyslav.savchenko@softcery.com" Date: Mon, 22 Sep 2025 13:37:34 +0300 Subject: [PATCH 12/18] chore(infra): returned create_database to true --- infra/environments/staging/variables.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/environments/staging/variables.tf b/infra/environments/staging/variables.tf index 8b045dd..b1365c2 100644 --- a/infra/environments/staging/variables.tf +++ b/infra/environments/staging/variables.tf @@ -32,7 +32,7 @@ variable "project_name" { variable "create_database" { description = "Whether to create a managed database" type = bool - default = false + default = true } variable "database_size" { From ec1663397842c79b464979f1052135661db66d75 Mon Sep 17 00:00:00 2001 From: "vladyslav.savchenko@softcery.com" Date: Mon, 22 Sep 2025 14:38:18 +0300 Subject: [PATCH 13/18] test: comment out db vars in workflow --- .github/workflows/release-staging.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release-staging.yml b/.github/workflows/release-staging.yml index a3354c4..64be71a 100644 --- a/.github/workflows/release-staging.yml +++ b/.github/workflows/release-staging.yml @@ -50,12 +50,12 @@ jobs: SUPABASE_SECRET_KEY=${{ env.SUPABASE_SECRET_KEY }} GOOGLE_OAUTH_CLIENT_ID=${{ env.GOOGLE_OAUTH_CLIENT_ID }} GOOGLE_OAUTH_SECRET=${{ env.GOOGLE_OAUTH_SECRET }} - DB_HOST=${{ env.DB_HOST }} - DB_PORT=${{ env.DB_PORT }} - DB_USER=${{ env.DB_USER }} - DB_PASSWORD=${{ env.DB_PASSWORD }} - DB_NAME=${{ env.DB_NAME }} - DB_CA="${{ env.DB_CA }}" +# DB_HOST=${{ env.DB_HOST }} +# DB_PORT=${{ env.DB_PORT }} +# DB_USER=${{ env.DB_USER }} +# DB_PASSWORD=${{ env.DB_PASSWORD }} +# DB_NAME=${{ env.DB_NAME }} +# DB_CA="${{ env.DB_CA }}" STRIPE_API_KEY=${{ env.STRIPE_API_KEY }} STRIPE_WEBHOOK_SIGNING_SECRET=${{ env.STRIPE_WEBHOOK_SIGNING_SECRET }} JWT_SECRET=${{ env.JWT_SECRET }} From 3bcd0f30e6bd28a2f8e5e71fca0de5b9d02a7f27 Mon Sep 17 00:00:00 2001 From: "vladyslav.savchenko@softcery.com" Date: Mon, 22 Sep 2025 14:43:27 +0300 Subject: [PATCH 14/18] test: remove db vars in workflow --- .github/workflows/release-staging.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/release-staging.yml b/.github/workflows/release-staging.yml index 64be71a..49aa9f3 100644 --- a/.github/workflows/release-staging.yml +++ b/.github/workflows/release-staging.yml @@ -27,12 +27,6 @@ jobs: SUPABASE_SECRET_KEY: ${{secrets.SUPABASE_SECRET_KEY}} GOOGLE_OAUTH_CLIENT_ID: ${{secrets.GOOGLE_OAUTH_CLIENT_ID}} GOOGLE_OAUTH_SECRET: ${{secrets.GOOGLE_OAUTH_SECRET}} - DB_HOST: ${{secrets.DB_HOST}} - DB_PORT: ${{secrets.DB_PORT}} - DB_USER: ${{secrets.DB_USER}} - DB_PASSWORD: ${{secrets.DB_PASSWORD}} - DB_NAME: ${{secrets.DB_NAME}} - DB_CA: ${{secrets.DB_CA}} STRIPE_API_KEY: ${{secrets.STRIPE_API_KEY}} STRIPE_WEBHOOK_SIGNING_SECRET: ${{secrets.STRIPE_WEBHOOK_SIGNING_SECRET}} JWT_SECRET: ${{secrets.JWT_SECRET}} @@ -50,12 +44,6 @@ jobs: SUPABASE_SECRET_KEY=${{ env.SUPABASE_SECRET_KEY }} GOOGLE_OAUTH_CLIENT_ID=${{ env.GOOGLE_OAUTH_CLIENT_ID }} GOOGLE_OAUTH_SECRET=${{ env.GOOGLE_OAUTH_SECRET }} -# DB_HOST=${{ env.DB_HOST }} -# DB_PORT=${{ env.DB_PORT }} -# DB_USER=${{ env.DB_USER }} -# DB_PASSWORD=${{ env.DB_PASSWORD }} -# DB_NAME=${{ env.DB_NAME }} -# DB_CA="${{ env.DB_CA }}" STRIPE_API_KEY=${{ env.STRIPE_API_KEY }} STRIPE_WEBHOOK_SIGNING_SECRET=${{ env.STRIPE_WEBHOOK_SIGNING_SECRET }} JWT_SECRET=${{ env.JWT_SECRET }} From 7c325eba60f417bf464d339279af372bb2fc3829 Mon Sep 17 00:00:00 2001 From: "vladyslav.savchenko@softcery.com" Date: Mon, 22 Sep 2025 17:02:19 +0300 Subject: [PATCH 15/18] chore(api): removed test console logs --- api/src/application.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/api/src/application.ts b/api/src/application.ts index 001e680..4906483 100644 --- a/api/src/application.ts +++ b/api/src/application.ts @@ -18,15 +18,6 @@ export class Application { this.nodeArguments = FlagParser.getFlags(process.argv); } public async init() { - console.log('Application initialized:', { - db_name: process.env.DB_NAME, - db_host: process.env.DB_HOST, - db_password: process.env.DB_PASSWORD, - db_port: process.env.DB_PORT, - db_user: process.env.DB_USER, - }); - console.log("DB CA") - console.log(process.env.DB_CA) this.app = await NestFactory.create(AppModule, { bodyParser: true, }); From 7e84c213e40f99fb6b833be91930bf88884f6761 Mon Sep 17 00:00:00 2001 From: "vladyslav.savchenko@softcery.com" Date: Tue, 23 Sep 2025 13:32:18 +0300 Subject: [PATCH 16/18] infra: added logtail --- .github/workflows/release-staging.yml | 2 ++ infra/environments/staging/variables.tf | 2 +- infra/modules/app/main.tf | 12 ++++++------ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-staging.yml b/.github/workflows/release-staging.yml index 49aa9f3..637eb6d 100644 --- a/.github/workflows/release-staging.yml +++ b/.github/workflows/release-staging.yml @@ -30,6 +30,7 @@ jobs: STRIPE_API_KEY: ${{secrets.STRIPE_API_KEY}} STRIPE_WEBHOOK_SIGNING_SECRET: ${{secrets.STRIPE_WEBHOOK_SIGNING_SECRET}} JWT_SECRET: ${{secrets.JWT_SECRET}} + LOGTAIL_SOURCE_TOKEN: ${{secrets.LOGTAIL_SOURCE_TOKEN}} API_BASE_URL: ${{vars.API_BASE_URL}} steps: @@ -58,6 +59,7 @@ jobs: run: | sed -i "s/autoreplace_do_token/$DIGITAL_OCEAN_TOKEN/g" variables.tf sed -i "s/autoreplace_github_token/$GITHUB_TOKEN/g" variables.tf + sed -i "s/autoreplace_logtail_token/$LOGTAIL_SOURCE_TOKEN/g" variables.tf - name: Terraform init and apply working-directory: ./infra/environments/staging diff --git a/infra/environments/staging/variables.tf b/infra/environments/staging/variables.tf index b1365c2..450f81d 100644 --- a/infra/environments/staging/variables.tf +++ b/infra/environments/staging/variables.tf @@ -14,7 +14,7 @@ variable "do_token" { variable "logtail_source_token" { description = "Logtail Token for the application logs" type = string - default = "" + default = "autoreplace_logtail_token" } variable "github_token" { diff --git a/infra/modules/app/main.tf b/infra/modules/app/main.tf index 568ae1d..603a23c 100644 --- a/infra/modules/app/main.tf +++ b/infra/modules/app/main.tf @@ -144,13 +144,13 @@ resource "digitalocean_app" "saas_template" { port = var.api_http_port } - # log_destination { - # name = var.logtail_source_name + log_destination { + name = var.logtail_source_name - # logtail { - # token = var.logtail_source_token - # } - # } + logtail { + token = var.logtail_source_token + } + } } ingress { From ba8fe990747f003040ed639e678d82860af915bb Mon Sep 17 00:00:00 2001 From: "vladyslav.savchenko@softcery.com" Date: Tue, 23 Sep 2025 17:27:19 +0300 Subject: [PATCH 17/18] chore: changed actual branch in infra and gh workflow to staging --- .github/workflows/release-staging.yml | 1 - infra/environments/staging/main.tf | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/release-staging.yml b/.github/workflows/release-staging.yml index 637eb6d..691770c 100644 --- a/.github/workflows/release-staging.yml +++ b/.github/workflows/release-staging.yml @@ -6,7 +6,6 @@ on: push: branches: - staging - - infra/db-cluster concurrency: group: ${{ github.head_ref || github.ref }} diff --git a/infra/environments/staging/main.tf b/infra/environments/staging/main.tf index 3f06b07..63d94e1 100644 --- a/infra/environments/staging/main.tf +++ b/infra/environments/staging/main.tf @@ -43,7 +43,7 @@ module "saas_template_app" { catchall_document = "index.html" static_build_command = "pnpm run build:digitalocean" gh_repository = "softcery/saas-template" - branch = "infra/db-cluster" + branch = "staging" deploy_on_push = false do_token = var.do_token github_token = var.github_token From e9deb04d045bca1249b7a497b9ebaa1578ab031a Mon Sep 17 00:00:00 2001 From: "vladyslav.savchenko@softcery.com" Date: Wed, 24 Sep 2025 10:30:34 +0300 Subject: [PATCH 18/18] chore(infra): refactored the code by splitting in separate output and database files --- infra/environments/staging/main.tf | 12 ---- infra/environments/staging/outputs.tf | 12 ++++ infra/modules/app/database.tf | 29 ++++++++++ infra/modules/app/main.tf | 83 --------------------------- infra/modules/app/outputs.tf | 54 +++++++++++++++++ 5 files changed, 95 insertions(+), 95 deletions(-) create mode 100644 infra/environments/staging/outputs.tf create mode 100644 infra/modules/app/database.tf create mode 100644 infra/modules/app/outputs.tf diff --git a/infra/environments/staging/main.tf b/infra/environments/staging/main.tf index 63d94e1..b40316c 100644 --- a/infra/environments/staging/main.tf +++ b/infra/environments/staging/main.tf @@ -65,15 +65,3 @@ module "saas_template_app" { database_node_count = var.database_node_count database_region = var.database_region } - -# Output database connection info if database is created -output "database_uri" { - value = module.saas_template_app.database_uri - sensitive = true - description = "Database connection URI (if database is created)" -} - -output "app_url" { - value = module.saas_template_app.app_url - description = "The live URL of the deployed app" -} diff --git a/infra/environments/staging/outputs.tf b/infra/environments/staging/outputs.tf new file mode 100644 index 0000000..78625f5 --- /dev/null +++ b/infra/environments/staging/outputs.tf @@ -0,0 +1,12 @@ +# Database outputs +output "database_uri" { + value = module.saas_template_app.database_uri + sensitive = true + description = "Database connection URI (if database is created)" +} + +# Application outputs +output "app_url" { + value = module.saas_template_app.app_url + description = "The live URL of the deployed app" +} \ No newline at end of file diff --git a/infra/modules/app/database.tf b/infra/modules/app/database.tf new file mode 100644 index 0000000..2899e7e --- /dev/null +++ b/infra/modules/app/database.tf @@ -0,0 +1,29 @@ +# Database cluster and related resources +resource "digitalocean_database_cluster" "postgres" { + count = var.create_database ? 1 : 0 + name = "${var.name}-db" + engine = "pg" + version = "15" + size = var.database_size + region = var.database_region + node_count = var.database_node_count + + project_id = digitalocean_project.saas_template.id +} + +resource "digitalocean_database_db" "app_database" { + count = var.create_database ? 1 : 0 + cluster_id = digitalocean_database_cluster.postgres[0].id + name = "app" +} + +resource "digitalocean_database_user" "app_user" { + count = var.create_database ? 1 : 0 + cluster_id = digitalocean_database_cluster.postgres[0].id + name = "appuser" +} + +data "digitalocean_database_ca" "postgres" { + count = var.create_database ? 1 : 0 + cluster_id = digitalocean_database_cluster.postgres[0].id +} \ No newline at end of file diff --git a/infra/modules/app/main.tf b/infra/modules/app/main.tf index 603a23c..e6a48f6 100644 --- a/infra/modules/app/main.tf +++ b/infra/modules/app/main.tf @@ -188,86 +188,3 @@ resource "digitalocean_app" "saas_template" { } } } - -resource "digitalocean_database_cluster" "postgres" { - count = var.create_database ? 1 : 0 - name = "${var.name}-db" - engine = "pg" - version = "15" - size = var.database_size - region = var.database_region - node_count = var.database_node_count - - project_id = digitalocean_project.saas_template.id -} - -resource "digitalocean_database_db" "app_database" { - count = var.create_database ? 1 : 0 - cluster_id = digitalocean_database_cluster.postgres[0].id - name = "app" -} - -resource "digitalocean_database_user" "app_user" { - count = var.create_database ? 1 : 0 - cluster_id = digitalocean_database_cluster.postgres[0].id - name = "appuser" -} - -data "digitalocean_database_ca" "postgres" { - count = var.create_database ? 1 : 0 - cluster_id = digitalocean_database_cluster.postgres[0].id -} - -# Outputs for database connection info -output "database_uri" { - value = var.create_database ? digitalocean_database_cluster.postgres[0].uri : "" - sensitive = true - description = "Database connection URI" -} - -output "database_private_uri" { - value = var.create_database ? digitalocean_database_cluster.postgres[0].private_uri : "" - sensitive = true - description = "Private database connection URI" -} - -output "database_host" { - value = var.create_database ? digitalocean_database_cluster.postgres[0].host : "" - sensitive = true - description = "Database host" -} - -output "database_port" { - value = var.create_database ? digitalocean_database_cluster.postgres[0].port : "" - sensitive = true - description = "Database port" -} - -output "database_user" { - value = var.create_database ? digitalocean_database_user.app_user[0].name : "" - sensitive = true - description = "Database user" -} - -output "database_password" { - value = var.create_database ? digitalocean_database_user.app_user[0].password : "" - sensitive = true - description = "Database password" -} - -output "database_name" { - value = var.create_database ? digitalocean_database_db.app_database[0].name : "" - sensitive = true - description = "Database name" -} - -output "database_ca_certificate" { - value = var.create_database ? data.digitalocean_database_ca.postgres[0].certificate : "" - sensitive = true - description = "Database CA certificate" -} - -output "app_url" { - value = digitalocean_app.saas_template.live_url - description = "The live URL of the deployed app" -} diff --git a/infra/modules/app/outputs.tf b/infra/modules/app/outputs.tf new file mode 100644 index 0000000..e6f5e5f --- /dev/null +++ b/infra/modules/app/outputs.tf @@ -0,0 +1,54 @@ +# Application outputs +output "app_url" { + value = digitalocean_app.saas_template.live_url + description = "The live URL of the deployed app" +} + +# Database outputs +output "database_uri" { + value = var.create_database ? digitalocean_database_cluster.postgres[0].uri : "" + sensitive = true + description = "Database connection URI" +} + +output "database_private_uri" { + value = var.create_database ? digitalocean_database_cluster.postgres[0].private_uri : "" + sensitive = true + description = "Private database connection URI" +} + +output "database_host" { + value = var.create_database ? digitalocean_database_cluster.postgres[0].host : "" + sensitive = true + description = "Database host" +} + +output "database_port" { + value = var.create_database ? digitalocean_database_cluster.postgres[0].port : "" + sensitive = true + description = "Database port" +} + +output "database_user" { + value = var.create_database ? digitalocean_database_user.app_user[0].name : "" + sensitive = true + description = "Database user" +} + +output "database_password" { + value = var.create_database ? digitalocean_database_user.app_user[0].password : "" + sensitive = true + description = "Database password" +} + +output "database_name" { + value = var.create_database ? digitalocean_database_db.app_database[0].name : "" + sensitive = true + description = "Database name" +} + +output "database_ca_certificate" { + value = var.create_database ? data.digitalocean_database_ca.postgres[0].certificate : "" + sensitive = true + description = "Database CA certificate" +} \ No newline at end of file