Skip to content

Commit 6bae9d5

Browse files
authored
chore(saas): one task per environment, and make the frontend follow it (#7483)
## The problem The `dev` profile hardcoded one project ref (`qacaivhsjtftfwtgjvva`) in five places: the ref, the Supabase URL, the publishable key, the datasource host and the meter endpoint. That made it both the shared environment everyone relies on *and* the only thing you could point the backend at. Testing an open SaaS PR meant hand-overriding all five via env just to reach that PR's Supabase preview branch, which is the only place the PR's migrations have actually been applied. Get it wrong and you see `relation "stirling_pdf.<new table>" does not exist` for a table the PR added, which is what happened on [#7414](#7414). ## One task per environment ```bash task dev:saas # backend + frontend + engine, against this PR's preview branch task staging:saas # backend + frontend + engine, against the shared v3 project task backend:dev:saas # backend only, preview branch task backend:staging:saas # backend only, v3 ``` | | how | vars | project | |---|---|---|---| | prod | `PROFILES=none` | `SAAS_DB_*` | the live one | | staging | `PROFILES=staging` | `SAAS_STAGING_*` | pinned to v3, always there | | dev | `PROFILES=dev` | `SAAS_DEV_*` | follows a SaaS PR's preview branch | `PROFILES` is still the underlying switch, so the old spelling keeps working. Production deliberately has no named task: reaching it should take a conscious `PROFILES=none`, not a tab-complete. **staging** is the old `dev` configuration, moved and kept pinned. The value of a shared environment is that it is still there tomorrow: reproduce a bug, paste a link to a colleague, share data. **dev** is parameterised by `SAAS_DEV_PROJECT_REF` and derives the Supabase URL, JWT issuer, JWKS, meter endpoint and (unless overridden) the database host from it. Switching which PR you are testing is one variable instead of five. With no ref set, `task backend:dev:saas` stops and says what to set rather than falling back. ## The frontend was the real gap `frontend/editor/.env` is committed and pins the **production** Supabase project, and nothing in the frontend knew about dev or staging. So `task dev:saas` gave you a backend on a preview branch and a login against prod, unless you happened to have hand-written `frontend/editor/.env.saas.local`. The dev tasks now read the backend's env files and derive `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY` from the same project ref the backend resolved, so the two halves cannot point at different projects. Nothing to keep in sync by hand, no new vite mode, and `SAAS_ENV=prod` opts back out to the committed values. ## Where to put your local values Two files, both gitignored, neither ever committed: **`app/.env.saas.local`** is the only one you normally need. The tasks load it for the backend *and* the frontend. ```bash # staging: everything else is already defaulted, so this is all it takes SAAS_STAGING_DB_PASSWORD=... # dev: the preview branch of the PR you are testing, from its "Supabase Preview" check. # A branch has its OWN password and API keys; the parent project's will not authenticate. SAAS_DEV_PROJECT_REF=... SAAS_DEV_DB_PASSWORD=... SAAS_DEV_PUBLISHABLE_KEY=... # prod, if you ever need it SAAS_DB_PROJECT_REF=... SAAS_DB_URL=... SAAS_DB_PASSWORD=... SUPABASE_EDGE_FUNCTION_SECRET=... ``` **`frontend/editor/.env.saas.local`** is no longer needed for choosing a Supabase project, and is best left empty or deleted. If you have one from before this PR, note that the task-supplied values now win, which is the point: the frontend follows the backend. **A blank is not the same as absent.** A dotenv line with an empty value still *sets* the variable, and Spring's `${VAR:default}` only falls back when a variable is absent. So `.env.saas` lists what you must set as blanks, and leaves out the two `*_DB_URL` overrides, which have real defaults to fall back to. This is not theoretical, see below. Committed `app/.env.saas` holds non-secret defaults only. Real secrets are passwords, the edge-function secret and service-role keys. Project refs and publishable keys are neither: a ref is the public `<ref>.supabase.co` subdomain and a publishable key ships in the browser bundle by design, which is why `frontend/editor/.env` has always carried prod's. ## Three bugs found while building the tasks All three were in this PR's own earlier commits, and all three were caught by actually booting things rather than by reading the config. **staging could not boot at all.** A blank `SAAS_STAGING_DB_URL=` in `.env.saas` set the variable to empty, so `${SAAS_STAGING_DB_URL:jdbc:...}` resolved to `""` and startup failed with `spring.datasource.url is required when the saas profile is active`. The file already carried a comment warning about exactly this; it had only been applied to the dev block. The original verification for this PR was "placeholders resolve" and "the task parses", neither of which boots anything. **The dev to staging fallback ran `ddl-auto=update` against shared v3.** The dev profile sets `update`, which is right for a disposable preview branch, and separately fell back to staging's project ref. Together that meant Hibernate was free to reconcile tables that RLS policies depend on. `application-staging.properties` pins `none`, but that only applies when the staging profile is the active one, which it was not on the fallback path. There is no fallback now: with no ref the task stops before gradle, and the frontend fails the same way, both naming the variable. **`PROFILES=` never selected production.** Go template `default` treats `""` as absent, so it silently resolved back to `dev`. It is `PROFILES=none` now. ## Two choices worth reviewing **Staging keeps its committed project ref**, now as a `${SAAS_STAGING_PROJECT_REF:...}` default in one place, with the URL, database host and meter endpoint all derived from it. So staging still works with zero setup, and repointing it is one variable. Nothing in CI referenced the ref or the profile. Its publishable key default carries no inline `gitleaks:allow`: a trailing comment in a `.properties` file is part of the value, so the pragma ended up inside the key. It is in `.gitleaksignore` instead. **`SAAS_DEV_DB_URL` still overrides the whole URL**, so a branch needing the pooler host rather than the direct one is reachable without touching committed config. ## Verification - `task backend:staging:saas` boots against v3 and serves `200`. It could not boot before this commit. - `task backend:dev:saas` with no ref stops before gradle naming the variable, and `PROFILES=none` still reaches production. `task frontend:dev:saas` fails the same way; `SAAS_ENV=staging` still resolves with no local config. - Frontend routing picks the SaaS runner for dev/staging and the plain runner for prod; the derivation returns the right URL and key for each. - Vite's `process.env` precedence and Task's dotenv/env semantics were measured, not assumed. That is how one trap surfaced: Task sets an `env:` key even when its value resolves to empty, and Vite treats an empty `process.env` `VITE_*` as authoritative over a committed `.env`. Putting the Supabase vars on the shared `dev:_run` would have blanked Supabase config for the core, proprietary and desktop dev servers, so the SaaS path has its own runner. - `:saas:spotlessApply` and `:saas:compileJava` green. `DevProfileProjectNotice` becomes `SaasProjectNotice` and covers both profiles, stating the project ref and `ddl-auto` at startup so which environment you are on is never a guess. No behaviour change for prod: the `saas` profile is untouched.
1 parent 0f8803f commit 6bae9d5

8 files changed

Lines changed: 287 additions & 52 deletions

File tree

.gitleaksignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,8 @@ app/core/src/main/java/stirling/software/SPDF/pdf/signature/CreateSignatureBase.
2727
# Supabase publishable key (public by design, RLS-protected) used as a CI fallback
2828
# default in the tauri-build workflow when the GitHub secret is unset - not a real secret.
2929
.github/workflows/tauri-build.yml:generic-api-key:402
30+
31+
# Staging Supabase publishable key (public by design). Ignored here rather than with an
32+
# inline gitleaks:allow because a trailing comment in a .properties file is part of the
33+
# value, so the pragma would end up inside the key.
34+
app/saas/src/main/resources/application-staging.properties:generic-api-key:16

.taskfiles/backend.yml

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,57 @@ tasks:
5757
- cmd: ./gradlew clean bootRun -PbuildWithFrontend=true
5858
platforms: [linux, darwin]
5959

60+
# SaaS backend. dev:saas -> the PR's preview branch, staging:saas -> shared v3,
61+
# PROFILES=none -> production against your own SAAS_DB_*. Production has no named
62+
# task on purpose. Use `none`, not an empty value: Go template `default` treats ""
63+
# as absent and would resolve back to dev.
64+
6065
dev:saas:
61-
desc: "Start backend in SaaS flavor against Supabase"
62-
# `dotenv:` reads from the root Taskfile's directory (".") because this
63-
# subtaskfile is included with `dir: .`.
66+
desc: "Start SaaS backend against the current PR's Supabase preview branch"
67+
dotenv: ['app/.env.saas.local', 'app/.env.saas']
68+
vars:
69+
PROFILES: '{{.PROFILES | default "dev"}}'
70+
cmds:
71+
# Don't move this check into a `sh:` var: dotenv is visible in cmds but not
72+
# during var evaluation, so the test would always see an empty value.
73+
- cmd: |
74+
if [ "{{.PROFILES}}" = "dev" ] && [ -z "${SAAS_DEV_PROJECT_REF:-}" ]; then
75+
echo ">> SAAS_DEV_PROJECT_REF is not set."
76+
echo ">> Testing a SaaS PR? Put its ref, DB password and publishable key in app/.env.saas.local."
77+
echo ">> Wanted the shared v3 project? Use 'task backend:staging:saas' instead."
78+
exit 1
79+
fi
80+
- task: _run:saas
81+
vars:
82+
PORT: '{{.PORT}}'
83+
PROFILES: '{{.PROFILES}}'
84+
AIENGINE_URL: '{{.AIENGINE_URL}}'
85+
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}'
86+
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}'
87+
88+
staging:saas:
89+
desc: "Start SaaS backend against the shared v3 staging project"
90+
cmds:
91+
- task: _run:saas
92+
vars:
93+
PORT: '{{.PORT}}'
94+
PROFILES: staging
95+
AIENGINE_URL: '{{.AIENGINE_URL}}'
96+
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}'
97+
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}'
98+
99+
_run:saas:
100+
internal: true
64101
dotenv: ['app/.env.saas.local', 'app/.env.saas']
65102
ignore_error: true
66103
vars:
67104
PORT: '{{.PORT | default "8080"}}'
68-
# Override to "" to run the pure `saas` profile against your own SAAS_DB_*.
69105
PROFILES: '{{.PROFILES | default "dev"}}'
106+
# Built here rather than inline in the cmds below: the Windows line is an
107+
# unquoted YAML scalar wrapping a cmd.exe string, so a nested {{if ne .X
108+
# "none"}} needs escaped quotes that reach the Go template as literal
109+
# backslashes and fail with `unexpected "\" in operand`.
110+
PROFILE_ARGS: '{{if ne .PROFILES "none"}}--spring.profiles.include={{.PROFILES}}{{end}}'
70111
AIENGINE_URL: '{{.AIENGINE_URL | default ""}}'
71112
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED | default "false"}}'
72113
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS | default "120"}}'
@@ -77,9 +118,11 @@ tasks:
77118
AIENGINE_ENABLED: '{{.AIENGINE_ENABLED}}'
78119
AIENGINE_TIMEOUTSECONDS: '{{.AIENGINE_TIMEOUTSECONDS}}'
79120
cmds:
80-
- cmd: cmd /c ".\gradlew.bat :stirling-pdf:bootRun {{if .PROFILES}}--args=\"--spring.profiles.include={{.PROFILES}}\"{{end}}"
121+
# PROFILE_ARGS is empty when PROFILES=none, i.e. the bare `saas` profile
122+
# against SAAS_DB_* (production).
123+
- cmd: cmd /c ".\gradlew.bat :stirling-pdf:bootRun {{if .PROFILE_ARGS}}--args=\"{{.PROFILE_ARGS}}\"{{end}}"
81124
platforms: [windows]
82-
- cmd: ./gradlew :stirling-pdf:bootRun {{if .PROFILES}}--args='--spring.profiles.include={{.PROFILES}}'{{end}}
125+
- cmd: ./gradlew :stirling-pdf:bootRun {{if .PROFILE_ARGS}}--args='{{.PROFILE_ARGS}}'{{end}}
83126
platforms: [linux, darwin]
84127

85128
build:

.taskfiles/frontend.yml

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ version: '3'
55
# mode flag) or use `--project editor/...` for tsc — so the editor lives
66
# under frontend/editor/ without each task needing a cd.
77

8+
vars:
9+
# Dev-only browser-tab label so concurrent worktrees are distinguishable. Only
10+
# the worktree folder basename (e.g. "wt1") is exposed — never the full path,
11+
# hostname, or user. Dropped from production builds.
12+
DEV_LABEL:
13+
sh: >-
14+
{{if eq OS "windows"}}powershell -NoProfile -Command '$root = git rev-parse --show-toplevel 2>$null; if (-not $root) { $root = (Get-Location).Path }; Split-Path -Leaf $root'{{else}}basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"{{end}}
15+
816
tasks:
917
install:
1018
desc: "Install dependencies"
@@ -80,16 +88,52 @@ tasks:
8088
OPEN: '{{.OPEN | default ""}}'
8189
env:
8290
BACKEND_URL: '{{.BACKEND_URL}}'
83-
# Dev-only browser-tab label so concurrent worktrees are distinguishable.
84-
# Only the worktree folder basename (e.g. "wt1") is exposed — never the
85-
# full path, hostname, or user. Consumed at dev-serve time by vite.config
86-
# and dropped from production builds.
87-
STIRLING_DEV_LABEL:
88-
sh: >-
89-
{{if eq OS "windows"}}powershell -NoProfile -Command '$root = git rev-parse --show-toplevel 2>$null; if (-not $root) { $root = (Get-Location).Path }; Split-Path -Leaf $root'{{else}}basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"{{end}}
91+
STIRLING_DEV_LABEL: '{{.DEV_LABEL}}'
9092
cmds:
9193
- npx vite editor --mode {{.MODE}} --port {{.PORT}}{{if .OPEN}} --open{{end}}
9294

95+
# Separate from dev:_run rather than a flag on it: Task sets an `env:` key even
96+
# when its value resolves to empty, and Vite treats an empty process.env VITE_* as
97+
# authoritative over the committed editor/.env, so folding these in blanks Supabase
98+
# config for the core, proprietary and desktop dev servers.
99+
dev:_run:saas:
100+
internal: true
101+
ignore_error: true
102+
# The backend's own env files, so both halves target one project. Paths are
103+
# relative to this taskfile's dir, `frontend`.
104+
dotenv: ['../app/.env.saas.local', '../app/.env.saas']
105+
vars:
106+
PORT: '{{.PORT | default "5173"}}'
107+
BACKEND_URL: '{{.BACKEND_URL | default "http://localhost:8080"}}'
108+
OPEN: '{{.OPEN | default ""}}'
109+
SAAS_ENV: '{{.SAAS_ENV | default "dev"}}'
110+
env:
111+
BACKEND_URL: '{{.BACKEND_URL}}'
112+
STIRLING_DEV_LABEL: '{{.DEV_LABEL}}'
113+
SAAS_ENV: '{{.SAAS_ENV}}'
114+
# A real process.env VITE_* beats a committed .env in Vite (loadEnv applies
115+
# process.env last), which is what lets this override editor/.env.
116+
#
117+
# These must stay `sh:`, not Go templates: dotenv values are visible to Task's
118+
# embedded shell but not to templates, where {{.SAAS_DEV_PROJECT_REF}} is
119+
# always empty.
120+
VITE_SUPABASE_URL:
121+
sh: |
122+
case "${SAAS_ENV:-dev}" in
123+
staging) ref="${SAAS_STAGING_PROJECT_REF:?set it in app/.env.saas.local}" ;;
124+
*) ref="${SAAS_DEV_PROJECT_REF:?set it in app/.env.saas.local, or run task staging:saas}" ;;
125+
esac
126+
echo "https://${ref}.supabase.co"
127+
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY:
128+
sh: |
129+
case "${SAAS_ENV:-dev}" in
130+
staging) echo "${SAAS_STAGING_PUBLISHABLE_KEY:?set it in app/.env.saas.local}" ;;
131+
*) echo "${SAAS_DEV_PUBLISHABLE_KEY:?set it in app/.env.saas.local}" ;;
132+
esac
133+
cmds:
134+
- 'echo ">> frontend Supabase target: $VITE_SUPABASE_URL"'
135+
- npx vite editor --mode saas --port {{.PORT}}{{if .OPEN}} --open{{end}}
136+
93137
dev:
94138
desc: "Start frontend dev server"
95139
cmds:
@@ -111,13 +155,23 @@ tasks:
111155
vars: { MODE: proprietary, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
112156

113157
dev:saas:
114-
desc: "Start frontend dev server in SaaS mode"
158+
desc: "Start frontend dev server in SaaS mode (SAAS_ENV=dev|staging|prod)"
115159
deps:
116160
- task: prepare
117161
vars: { MODE: saas }
118-
cmds:
119-
- task: dev:_run
120-
vars: { MODE: saas, PORT: '{{.PORT}}', BACKEND_URL: '{{.BACKEND_URL}}', OPEN: '{{.OPEN}}' }
162+
vars:
163+
SAAS_ENV: '{{.SAAS_ENV | default "dev"}}'
164+
# prod routes to the plain runner, which sets no VITE_SUPABASE_* and so leaves
165+
# the committed editor/.env alone.
166+
RUNNER: '{{if eq .SAAS_ENV "prod"}}dev:_run{{else}}dev:_run:saas{{end}}'
167+
cmds:
168+
- task: '{{.RUNNER}}'
169+
vars:
170+
MODE: saas
171+
PORT: '{{.PORT}}'
172+
BACKEND_URL: '{{.BACKEND_URL}}'
173+
OPEN: '{{.OPEN}}'
174+
SAAS_ENV: '{{.SAAS_ENV}}'
121175

122176
dev:desktop:
123177
desc: "Start frontend dev server in desktop mode"

Taskfile.yml

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,22 @@ tasks:
9999
BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}'
100100
OPEN: "true"
101101

102+
# Set SAAS_DEV_PROJECT_REF in app/.env.saas.local to pick the PR.
102103
dev:saas:
103-
desc: "Start SaaS backend + frontend concurrently on free ports"
104+
desc: "Start SaaS backend + frontend + engine against the current PR's preview branch"
104105
cmds:
105106
- task: dev:_all
106-
vars: { FRONTEND: saas, BACKEND: saas }
107+
vars: { FRONTEND: saas, BACKEND: saas, SAAS_ENV: dev }
108+
109+
staging:saas:
110+
desc: "Start SaaS backend + frontend + engine against the shared v3 staging project"
111+
cmds:
112+
- task: dev:_all
113+
vars:
114+
FRONTEND: saas
115+
BACKEND: saas
116+
BACKEND_TASK: backend:staging:saas
117+
SAAS_ENV: staging
107118

108119
dev:all:
109120
desc: "Start backend + frontend + engine concurrently on free ports"
@@ -115,6 +126,9 @@ tasks:
115126
vars:
116127
FRONTEND: '{{.FRONTEND | default "proprietary"}}'
117128
BACKEND: '{{.BACKEND | default "proprietary"}}'
129+
BACKEND_TASK: '{{.BACKEND_TASK | default (printf "backend:dev:%s" .BACKEND)}}'
130+
# Only meaningful to the saas frontend; every other flavor ignores it.
131+
SAAS_ENV: '{{.SAAS_ENV | default ""}}'
118132
PORTS:
119133
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080 5173 5001{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173 5001{{end}}'
120134
BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}'
@@ -124,7 +138,7 @@ tasks:
124138
- task: engine:dev
125139
vars:
126140
PORT: '{{.ENGINE_PORT}}'
127-
- task: 'backend:dev:{{.BACKEND}}'
141+
- task: '{{.BACKEND_TASK}}'
128142
vars:
129143
PORT: '{{.BACKEND_PORT}}'
130144
AIENGINE_URL: 'http://localhost:{{.ENGINE_PORT}}'
@@ -134,6 +148,7 @@ tasks:
134148
PORT: '{{.FRONTEND_PORT}}'
135149
BACKEND_URL: 'http://localhost:{{.BACKEND_PORT}}'
136150
OPEN: "true"
151+
SAAS_ENV: '{{.SAAS_ENV}}'
137152

138153
# ============================================================
139154
# Build

app/.env.saas

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,52 @@
1-
###############################################################################
2-
# Stirling-PDF SaaS environment defaults.
1+
# Stirling-PDF SaaS environment defaults. Committed, non-secret. Real values for secrets go in
2+
# .env.saas.local, which is loaded first and wins. Do not commit that file.
33
#
4-
# This file is committed and provides non-secret defaults loaded by
5-
# `task backend:dev:saas`. Put real values for secrets (passwords, project
6-
# refs, edge function secrets) in `.env.saas.local` - any variable set there
7-
# takes precedence over what's defined here.
4+
# Three environments, each deriving its Supabase URLs, JWT issuer and JWKS from one project ref:
85
#
9-
# DO NOT commit `.env.saas.local`. Only `.env.saas` is checked in.
10-
###############################################################################
6+
# prod PROFILES=none SAAS_DB_* the live project
7+
# staging PROFILES=staging SAAS_STAGING_* pinned to v3, always there
8+
# dev PROFILES=dev SAAS_DEV_* follows a SaaS PR's preview branch
9+
#
10+
# dev is the default for `task backend:dev:saas`. Use staging for somewhere stable; use dev when
11+
# testing an open SaaS PR, since its preview branch is the only place those migrations are applied.
1112

12-
# ---------- Supabase project ----------
13+
# ---------- Supabase project (prod / no-profile) ----------
1314
# Project reference (the subdomain part of <ref>.supabase.co). Required.
1415
# Set in .env.saas.local.
1516
SAAS_DB_PROJECT_REF=
1617

1718
# Edge function secret used by billing/license rollup calls. Set in .env.saas.local.
1819
SUPABASE_EDGE_FUNCTION_SECRET=
1920

20-
# ---------- Database (saas profile) ----------
21-
# Direct JDBC URL to the Supabase Postgres. Required when running the plain
22-
# `saas` profile (i.e. without `--spring.profiles.include=dev`).
21+
# ---------- Database (no profile) ----------
22+
# Direct JDBC URL to the Supabase Postgres. Required when running without
23+
# `--spring.profiles.include=...`.
2324
# Example: jdbc:postgresql://db.<project-ref>.supabase.co:5432/postgres
2425
SAAS_DB_URL=
2526
SAAS_DB_USERNAME=postgres
2627
SAAS_DB_PASSWORD=
2728

28-
# ---------- Database (dev profile overrides) ----------
29-
# Used when `--spring.profiles.include=dev` is active. The dev profile
30-
# defaults the URL/username to the shared dev Supabase project, but the
31-
# password must still be provided in .env.saas.local.
32-
SAAS_DEV_DB_URL=
29+
# ---------- staging profile ----------
30+
# The shared long-lived v3 project. application-staging.properties defaults the ref,
31+
# URL, database host and meter endpoint, so staging needs only the password, in
32+
# .env.saas.local. Set SAAS_STAGING_PROJECT_REF to repoint it; everything derives.
33+
#
34+
# The ref and publishable key are duplicated here because the task derives the
35+
# frontend's VITE_SUPABASE_* from them and a shell cannot read a Spring default.
36+
# Neither is secret: the ref is a public subdomain, the key ships in the bundle.
37+
SAAS_STAGING_PROJECT_REF=qacaivhsjtftfwtgjvva
38+
SAAS_STAGING_PUBLISHABLE_KEY=sb_publishable_nIM8y-9ARPE7EzQwAQHKMg_40fCN6kY # gitleaks:allow
39+
SAAS_STAGING_DB_USERNAME=postgres
40+
SAAS_STAGING_DB_PASSWORD=
41+
42+
# ---------- dev profile ----------
43+
# The SaaS PR's Supabase preview branch. Take the ref from that PR's "Supabase
44+
# Preview" check; the profile derives URL, JWT issuer, JWKS, meter endpoint and
45+
# database host from it, so this one value follows a different PR.
46+
#
47+
# A preview branch has its own password and keys; the parent project's will not
48+
# authenticate. Both go in .env.saas.local, along with the ref.
49+
SAAS_DEV_PROJECT_REF=
50+
SAAS_DEV_PUBLISHABLE_KEY=
3351
SAAS_DEV_DB_USERNAME=postgres
3452
SAAS_DEV_DB_PASSWORD=
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package stirling.software.saas.config;
2+
3+
import org.springframework.beans.factory.annotation.Value;
4+
import org.springframework.boot.context.event.ApplicationReadyEvent;
5+
import org.springframework.context.annotation.Profile;
6+
import org.springframework.context.event.EventListener;
7+
import org.springframework.core.env.Environment;
8+
import org.springframework.stereotype.Component;
9+
10+
import lombok.extern.slf4j.Slf4j;
11+
12+
/** Logs which Supabase project this backend is talking to, and its schema policy. */
13+
@Slf4j
14+
@Component
15+
@Profile({"dev", "staging"})
16+
public class SaasProjectNotice {
17+
18+
private final Environment environment;
19+
private final String projectRef;
20+
private final String ddlAuto;
21+
22+
public SaasProjectNotice(
23+
Environment environment,
24+
@Value("${app.supabase.project-ref:unknown}") String projectRef,
25+
@Value("${spring.jpa.hibernate.ddl-auto:none}") String ddlAuto) {
26+
this.environment = environment;
27+
this.projectRef = projectRef;
28+
this.ddlAuto = ddlAuto;
29+
}
30+
31+
@EventListener(ApplicationReadyEvent.class)
32+
public void announceProject() {
33+
boolean staging = environment.matchesProfiles("staging");
34+
if (staging) {
35+
log.info(
36+
"""
37+
SaaS staging profile: Supabase project {}, ddl-auto={}. This is the SHARED \
38+
long-lived environment, so its data and schema are not yours alone. Testing an \
39+
open SaaS PR? Use that PR's preview branch instead \
40+
(SAAS_DEV_PROJECT_REF in app/.env.saas.local); staging will not have its \
41+
migrations.\
42+
""",
43+
projectRef,
44+
ddlAuto);
45+
return;
46+
}
47+
log.info(
48+
"SaaS dev profile: Supabase preview branch {}, ddl-auto={}. Disposable, so Hibernate"
49+
+ " is allowed to add the inherited tables the migrations do not create.",
50+
projectRef,
51+
ddlAuto);
52+
}
53+
}

0 commit comments

Comments
 (0)