Skip to content

Commit 24c3103

Browse files
committed
feat: render backend + cloudflare DNS + prod-ready fixes
- Add render.yaml: Go/Fiber backend on Render free plan, auto-deploy, custom domain api.senzen.pantorn.site, secrets in dashboard (sync:false) - Add infra/terraform/cloudflare.tf: CNAMEs for senzen (Vercel) and api.senzen (Render), DNS-only so providers can issue their own certs - Update terraform.yml CI: add CLOUDFLARE_API_TOKEN + CLOUDFLARE_ZONE_ID secrets, trigger on render.yaml changes - Fix Vercel env vars: environment -> target, add sensitive = false (vercel/vercel v2 schema change) - Fix backend: godotenv no longer fatal (would crash on Render without .env), main.go uses PORT env var with 8080 fallback - Fix OAuth: RedirectURL now uses BACKEND_URL env var instead of hardcoded localhost:8080 - Fix database DSN: sslmode=disable -> sslmode=require (Supabase needs TLS) - Drop .github/workflows/backend-deploy.yml (replaced by render.yaml)
1 parent 67591cb commit 24c3103

12 files changed

Lines changed: 362 additions & 180 deletions

File tree

.github/workflows/backend-deploy.yml

Lines changed: 0 additions & 103 deletions
This file was deleted.

.github/workflows/terraform.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches: [main]
66
paths:
77
- "infra/terraform/**"
8+
- "render.yaml"
89
pull_request:
910
branches: [main]
1011
paths:
@@ -44,10 +45,14 @@ jobs:
4445
if: github.event_name == 'pull_request'
4546
run: terraform plan -no-color -input=false
4647
env:
47-
TF_VAR_vercel_api_token: ${{ secrets.VERCEL_API_TOKEN }}
48+
TF_VAR_vercel_api_token: ${{ secrets.VERCEL_API_TOKEN }}
49+
TF_VAR_cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
50+
TF_VAR_cloudflare_zone_id: ${{ secrets.CLOUDFLARE_ZONE_ID }}
4851

4952
- name: Terraform Apply
5053
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
5154
run: terraform apply -auto-approve -input=false
5255
env:
53-
TF_VAR_vercel_api_token: ${{ secrets.VERCEL_API_TOKEN }}
56+
TF_VAR_vercel_api_token: ${{ secrets.VERCEL_API_TOKEN }}
57+
TF_VAR_cloudflare_api_token: ${{ secrets.CLOUDFLARE_API_TOKEN }}
58+
TF_VAR_cloudflare_zone_id: ${{ secrets.CLOUDFLARE_ZONE_ID }}

backend/config/config.go

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"log"
55
"os"
66

7-
// "fmt"
87
"github.com/joho/godotenv"
98
"golang.org/x/oauth2"
109
"golang.org/x/oauth2/github"
@@ -18,15 +17,23 @@ type Config struct {
1817

1918
var AppConfig Config
2019

20+
// backendURL returns the BACKEND_URL env var, or localhost for dev.
21+
// OAuth providers will redirect back to <backendURL>/<provider>_callback.
22+
func backendURL() string {
23+
if u := os.Getenv("BACKEND_URL"); u != "" {
24+
return u
25+
}
26+
return "http://localhost:8080"
27+
}
28+
2129
func GoogleConfig() oauth2.Config {
22-
err := godotenv.Load(".env")
23-
if err != nil {
24-
log.Fatalf("Some error occured. Err: %s", err)
30+
// godotenv is a dev convenience only — log a warning if missing.
31+
if err := godotenv.Load(".env"); err != nil {
32+
log.Printf("[config] no .env file found (expected in production): %v", err)
2533
}
2634

2735
AppConfig.GoogleLoginConfig = oauth2.Config{
28-
RedirectURL: "http://localhost:8080/google_callback",
29-
// RedirectURL: "http://localhost:3000/",
36+
RedirectURL: backendURL() + "/google_callback",
3037
ClientID: os.Getenv("GOOGLE_CLIENT_ID"),
3138
ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
3239
Scopes: []string{
@@ -40,17 +47,13 @@ func GoogleConfig() oauth2.Config {
4047
}
4148

4249
func GithubConfig() oauth2.Config {
43-
err := godotenv.Load(".env")
44-
if err != nil {
45-
log.Fatalf("Some error occured. Err: %s", err)
50+
if err := godotenv.Load(".env"); err != nil {
51+
log.Printf("[config] no .env file found (expected in production): %v", err)
4652
}
4753

4854
AppConfig.GitHubLoginConfig = oauth2.Config{
49-
RedirectURL: "http://localhost:8080/github_callback",
50-
// RedirectURL: "http://localhost:3000/",
51-
ClientID: os.Getenv("GITHUB_CLIENT_ID"),
52-
// RedirectURL: fmt.Sprintf(
53-
// "https://github.com/login/oauth/authorize?scope=user:repo&client_id=%s&redirect_uri=%s", os.Getenv("GITHUB_CLIENT_ID"), "http://localhost:8080/github_callback"),
55+
RedirectURL: backendURL() + "/github_callback",
56+
ClientID: os.Getenv("GITHUB_CLIENT_ID"),
5457
ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
5558
Scopes: []string{"user", "repo"},
5659
Endpoint: github.Endpoint,

backend/config/database.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,12 @@ var DB *gorm.DB
1919
func ConnectDatabase() {
2020

2121
// Set up the DSN and GORM logger
22+
// sslmode=require is needed for Supabase (and any managed Postgres
23+
// that enforces TLS). The local dev setup also works with require
24+
// if you trust the local server; switch to "disable" only if your
25+
// local Postgres rejects TLS.
2226
dsn := fmt.Sprintf(
23-
"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Bangkok",
27+
"host=%s user=%s password=%s dbname=%s port=%s sslmode=require TimeZone=Asia/Bangkok",
2428
os.Getenv("DB_HOST"), os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_NAME"), os.Getenv("DB_PORT"),
2529
)
2630

backend/main.go

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,59 +2,46 @@ package main
22

33
import (
44
"log"
5-
// "os"
5+
"os"
66

7-
"github.com/gofiber/contrib/websocket"
87
"github.com/gofiber/fiber/v2"
9-
// jwtware "github.com/gofiber/jwt/v3"
108
"github.com/joho/godotenv"
119
"github.com/long104/CashWise/config"
1210
"github.com/long104/CashWise/middleware"
1311
"github.com/long104/CashWise/routes"
1412
)
1513

1614
func main() {
17-
err := godotenv.Load(".env")
18-
if err != nil {
19-
log.Fatalf("Some error occurred. Err: %s", err)
15+
// godotenv is a dev convenience only — in production (Render)
16+
// env vars come from the platform and .env doesn't exist.
17+
// Log a warning instead of crashing if .env is missing.
18+
if err := godotenv.Load(".env"); err != nil {
19+
log.Printf("[startup] no .env file found (expected in production): %v", err)
2020
}
21+
22+
port := os.Getenv("PORT")
23+
if port == "" {
24+
port = "8080" // local dev default; Render sets PORT=10000
25+
}
26+
2127
config.ConnectDatabase()
2228
app := fiber.New()
2329
api := app.Group("/api")
2430

25-
2631
app.Use(middleware.CORSMiddleware())
2732

28-
2933
app.Get("/health", func(c *fiber.Ctx) error {
3034
return c.SendString("health check ok")
3135
})
3236

33-
app.Use("/ws", func(c *fiber.Ctx) error {
34-
// IsWebSocketUpgrade returns true if the client
35-
// requested upgrade to the WebSocket protocol.
36-
if websocket.IsWebSocketUpgrade(c) {
37-
c.Locals("allowed", true)
38-
return c.Next()
39-
}
40-
return fiber.ErrUpgradeRequired
41-
})
37+
app.Get("/validate-token", middleware.ValidateToken)
4238

43-
// routes.WsRoutes(app)
4439
routes.SetupOAuthRoutes(api)
4540
routes.SetupAuthRoutes(api)
46-
47-
// app.Use(jwtware.New(jwtware.Config{
48-
// SigningKey: []byte(os.Getenv("jwtSecretKey")),
49-
// }))
50-
51-
// app.Use("/admin", middleware.checkMiddleware)
52-
// app.Use("/books", AuthRequired)
53-
app.Get("/validate-token", middleware.ValidateToken)
54-
5541
routes.SetupRoutes(api)
5642

57-
// setupRoutes(app)
58-
59-
app.Listen(":8080")
43+
log.Printf("[startup] listening on :%s", port)
44+
if err := app.Listen(":" + port); err != nil {
45+
log.Fatalf("[startup] server failed: %v", err)
46+
}
6047
}

infra/terraform/cloudflare.tf

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# =============================================================
2+
# Cloudflare DNS — senzen.pantorn.site
3+
# =============================================================
4+
# This file manages the DNS records for the Senzen project.
5+
# Records:
6+
# senzen CNAME → cname.vercel-dns.com (frontend)
7+
# api.senzen CNAME → <render-service>.onrender.com (backend)
8+
#
9+
# Both are DNS-only (proxied = false) so Vercel/Render can
10+
# issue and serve their own TLS certificates. The orange-cloud
11+
# proxy would break ACME HTTP-01 challenges for cert issuance.
12+
# =============================================================
13+
14+
# ─────────────────────────────────────────────────────────────
15+
# Frontend: senzen.pantorn.site → Vercel
16+
# ─────────────────────────────────────────────────────────────
17+
resource "cloudflare_record" "senzen_frontend" {
18+
zone_id = var.cloudflare_zone_id
19+
name = split(".", var.frontend_domain)[0] # "senzen" from "senzen.pantorn.site"
20+
type = "CNAME"
21+
value = var.vercel_cname_target
22+
proxied = false
23+
comment = "Senzen frontend → Vercel"
24+
}
25+
26+
# ─────────────────────────────────────────────────────────────
27+
# Backend: api.senzen.pantorn.site → Render
28+
# ─────────────────────────────────────────────────────────────
29+
resource "cloudflare_record" "senzen_backend" {
30+
zone_id = var.cloudflare_zone_id
31+
name = "${var.backend_subdomain}.${split(".", var.frontend_domain)[0]}" # "api.senzen"
32+
type = "CNAME"
33+
value = "${var.render_service_name}.onrender.com"
34+
proxied = false
35+
comment = "Senzen backend → Render (${var.render_service_name}.onrender.com)"
36+
}

infra/terraform/outputs.tf

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
output "vercel_url" {
2-
description = "Vercel frontend deployment URL"
2+
description = "Vercel frontend deployment URL (auto-assigned)"
33
value = "https://${vercel_project.cashwise.name}.vercel.app"
44
}
55

@@ -8,7 +8,32 @@ output "vercel_project_id" {
88
value = vercel_project.cashwise.id
99
}
1010

11+
output "frontend_url" {
12+
description = "Public frontend URL (custom domain)"
13+
value = "https://${var.frontend_domain}"
14+
}
15+
1116
output "backend_url" {
12-
description = "SnapDeploy backend URL"
13-
value = var.snapdeploy_backend_url
17+
description = "Public backend URL (custom domain + /api prefix)"
18+
value = "https://${var.backend_subdomain}.${var.frontend_domain}"
19+
}
20+
21+
output "backend_api_url" {
22+
description = "Backend API base path the frontend calls"
23+
value = "https://${var.backend_subdomain}.${var.frontend_domain}/api"
24+
}
25+
26+
output "render_onrender_url" {
27+
description = "Render's default onrender.com URL (DNS target in Cloudflare)"
28+
value = "https://${var.render_service_name}.onrender.com"
29+
}
30+
31+
output "cloudflare_record_senzen" {
32+
description = "Cloudflare CNAME record for the frontend"
33+
value = cloudflare_record.senzen_frontend.hostname
34+
}
35+
36+
output "cloudflare_record_api" {
37+
description = "Cloudflare CNAME record for the backend"
38+
value = cloudflare_record.senzen_backend.hostname
1439
}

0 commit comments

Comments
 (0)