@moifort/stack 0.0.0-stage → 0.1.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.
- package/CHANGELOG.md +18 -0
- package/README.md +26 -2
- package/bin/stack.ts +109 -0
- package/hooks/shutdown-simulators-after-push.sh +19 -0
- package/package.json +40 -4
- package/routines/daily-health.md +78 -0
- package/routines/deprecations.md +23 -0
- package/routines/sentry-autofix.md +76 -0
- package/routines/stack-upgrade.md +45 -0
- package/rules/00-collaboration.md +81 -0
- package/rules/01-workflow.md +81 -0
- package/rules/02-commits.md +71 -0
- package/rules/03-code-review.md +25 -0
- package/rules/04-push.md +75 -0
- package/rules/05-release.md +40 -0
- package/rules/06-changelog.md +48 -0
- package/rules/07-appstore.md +77 -0
- package/rules/08-copy.md +52 -0
- package/rules/09-backend.md +132 -0
- package/rules/10-ios.md +206 -0
- package/rules/11-ops.md +114 -0
- package/rules/12-ci.md +32 -0
- package/rules/13-ai.md +71 -0
- package/rules/14-infra.md +90 -0
- package/rules/15-tooling.md +81 -0
- package/rules/16-routines.md +69 -0
- package/rules/17-landing.md +76 -0
- package/skills/appstore/SKILL.md +53 -0
- package/skills/domain/SKILL.md +136 -0
- package/skills/graphql/SKILL.md +83 -0
- package/skills/ios-feature/SKILL.md +94 -0
- package/skills/stack-install/SKILL.md +35 -0
- package/skills/stack-upgrade/SKILL.md +27 -0
- package/src/catalogue.ts +33 -0
- package/src/config.ts +20 -0
- package/src/init.ts +63 -0
- package/src/migrations.ts +72 -0
- package/src/migrations.unit.test.ts +44 -0
- package/src/package.ts +8 -0
- package/src/render.ts +13 -0
- package/src/render.unit.test.ts +17 -0
- package/src/sync.int.test.ts +103 -0
- package/src/sync.ts +161 -0
- package/src/templates.ts +121 -0
- package/src/validate-content.ts +65 -0
- package/templates/backend/.env.example +10 -0
- package/templates/backend/.github/workflows/deploy.yml +151 -0
- package/templates/backend/.github/workflows/schema.yml +59 -0
- package/templates/backend/.github/workflows/test-feat.yml +40 -0
- package/templates/backend/.github/workflows/test-int.yml +40 -0
- package/templates/backend/.github/workflows/test-unit.yml +64 -0
- package/templates/backend/docs/api-evolution.md +67 -0
- package/templates/backend/firebase.json +21 -0
- package/templates/backend/firestore.indexes.json +4 -0
- package/templates/backend/firestore.rules +11 -0
- package/templates/backend/infra/auth.tf +54 -0
- package/templates/backend/infra/backend.tf +6 -0
- package/templates/backend/infra/cicd.tf +63 -0
- package/templates/backend/infra/firebase.tf +26 -0
- package/templates/backend/infra/firestore.tf +53 -0
- package/templates/backend/infra/function.tf +171 -0
- package/templates/backend/infra/main.tf +26 -0
- package/templates/backend/infra/observer.tf +18 -0
- package/templates/backend/infra/outputs.tf +30 -0
- package/templates/backend/infra/project.tf +64 -0
- package/templates/backend/infra/scheduler.tf +28 -0
- package/templates/backend/infra/secrets.tf +44 -0
- package/templates/backend/infra/state.tf +13 -0
- package/templates/backend/infra/terraform.tfvars.example +16 -0
- package/templates/backend/infra/variables.tf +104 -0
- package/templates/backend/infra/versions.tf +30 -0
- package/templates/backend/nitro.config.ts +40 -0
- package/templates/backend/package.json +51 -0
- package/templates/backend/scripts/deprecations/inspector-rule.ts +29 -0
- package/templates/backend/scripts/deprecations/status.ts +50 -0
- package/templates/backend/scripts/deprecations/status.unit.test.ts +57 -0
- package/templates/backend/scripts/deprecations/usage.ts +79 -0
- package/templates/backend/scripts/deprecations/usage.unit.test.ts +64 -0
- package/templates/backend/scripts/deprecations.ts +224 -0
- package/templates/backend/scripts/export-graphql-schema.ts +11 -0
- package/templates/backend/scripts/generate-changelog-asset.ts +27 -0
- package/templates/backend/scripts/install-terraform.sh +61 -0
- package/templates/backend/scripts/tf +11 -0
- package/templates/backend/server/domain/changelog/business-rules.ts +60 -0
- package/templates/backend/server/domain/changelog/business-rules.unit.test.ts +88 -0
- package/templates/backend/server/domain/changelog/infrastructure/graphql/queries.ts +17 -0
- package/templates/backend/server/domain/changelog/infrastructure/graphql/types.ts +23 -0
- package/templates/backend/server/domain/changelog/primitives.ts +19 -0
- package/templates/backend/server/domain/changelog/query.ts +13 -0
- package/templates/backend/server/domain/changelog/query.unit.test.ts +19 -0
- package/templates/backend/server/domain/changelog/types.ts +9 -0
- package/templates/backend/server/domain/shared/graphql/builder.ts +52 -0
- package/templates/backend/server/domain/shared/graphql/errors.ts +19 -0
- package/templates/backend/server/domain/shared/graphql/scalars.ts +51 -0
- package/templates/backend/server/domain/shared/graphql/schema.ts +21 -0
- package/templates/backend/server/domain/shared/language.ts +26 -0
- package/templates/backend/server/domain/shared/page.ts +25 -0
- package/templates/backend/server/domain/shared/page.unit.test.ts +51 -0
- package/templates/backend/server/domain/shared/primitives.ts +50 -0
- package/templates/backend/server/domain/shared/primitives.unit.test.ts +25 -0
- package/templates/backend/server/domain/shared/types.ts +14 -0
- package/templates/backend/server/domain/user/command.ts +16 -0
- package/templates/backend/server/domain/user/infrastructure/graphql/inputs.ts +12 -0
- package/templates/backend/server/domain/user/infrastructure/graphql/mutations.ts +40 -0
- package/templates/backend/server/domain/user/infrastructure/graphql/queries.ts +13 -0
- package/templates/backend/server/domain/user/infrastructure/graphql/types.ts +35 -0
- package/templates/backend/server/domain/user/infrastructure/repository.ts +59 -0
- package/templates/backend/server/domain/user/mutations.feat.test.ts +42 -0
- package/templates/backend/server/domain/user/query.int.test.ts +49 -0
- package/templates/backend/server/domain/user/query.ts +34 -0
- package/templates/backend/server/domain/user/types.ts +20 -0
- package/templates/backend/server/domain/user/use-case.ts +18 -0
- package/templates/backend/server/middleware/auth.ts +54 -0
- package/templates/backend/server/plugins/00-sentry.ts +74 -0
- package/templates/backend/server/plugins/01-request-log.ts +28 -0
- package/templates/backend/server/plugins/02-graphql.ts +69 -0
- package/templates/backend/server/plugins/03-boot-time.ts +21 -0
- package/templates/backend/server/routes/admin/migrate.post.ts +9 -0
- package/templates/backend/server/routes/app-config.get.ts +15 -0
- package/templates/backend/server/routes/graphql.ts +52 -0
- package/templates/backend/server/system/app-support.ts +17 -0
- package/templates/backend/server/system/config/index.ts +18 -0
- package/templates/backend/server/system/config/primitives.ts +20 -0
- package/templates/backend/server/system/config/primitives.unit.test.ts +13 -0
- package/templates/backend/server/system/config/types.ts +8 -0
- package/templates/backend/server/system/firebase.ts +19 -0
- package/templates/backend/server/system/identity.ts +18 -0
- package/templates/backend/server/system/logger.ts +119 -0
- package/templates/backend/server/system/logger.unit.test.ts +109 -0
- package/templates/backend/server/system/migration/migrations/index.ts +6 -0
- package/templates/backend/server/system/migration/primitives.ts +18 -0
- package/templates/backend/server/system/migration/runner.ts +52 -0
- package/templates/backend/server/system/migration/types.ts +16 -0
- package/templates/backend/server/system/read-meter.ts +100 -0
- package/templates/backend/server/system/read-meter.unit.test.ts +97 -0
- package/templates/backend/server/system/request-cache.ts +139 -0
- package/templates/backend/server/system/request-cache.unit.test.ts +166 -0
- package/templates/backend/server/system/secret-box.ts +49 -0
- package/templates/backend/server/system/secret-box.unit.test.ts +46 -0
- package/templates/backend/server/test/bdd.ts +12 -0
- package/templates/backend/server/test/fake-firestore.ts +483 -0
- package/templates/backend/server/utils/apollo.ts +13 -0
- package/templates/backend/server/utils/fetch.ts +5 -0
- package/templates/backend/server/utils/firestore.ts +91 -0
- package/templates/backend/server/utils/input.ts +36 -0
- package/templates/backend/server/utils/input.unit.test.ts +43 -0
- package/templates/backend/server/utils/slug.ts +21 -0
- package/templates/backend/tsconfig.json +16 -0
- package/templates/base/.github/workflows/renovate.yml +17 -0
- package/templates/base/CHANGELOG.fr.md +3 -0
- package/templates/base/CHANGELOG.md +7 -0
- package/templates/base/CLAUDE.md +34 -0
- package/templates/base/README.md +12 -0
- package/templates/base/biome.json +47 -0
- package/templates/base/bunfig.toml +2 -0
- package/templates/base/docs/adr/0001-built-on-the-polyforms-stack.md +21 -0
- package/templates/base/docs/data-recovery.md +116 -0
- package/templates/base/docs/ideas.md +4 -0
- package/templates/base/project-context.md +43 -0
- package/templates/base/renovate.json +37 -0
- package/templates/ios/.github/workflows/build-ios.yml +168 -0
- package/templates/ios/.github/workflows/ios-upload.yml +308 -0
- package/templates/ios/.github/workflows/release-ios.yml +178 -0
- package/templates/ios/.github/workflows/testflight-ios.yml +57 -0
- package/templates/ios/infra/Gemfile +5 -0
- package/templates/ios/infra/fastlane/Appfile +1 -0
- package/templates/ios/infra/fastlane/Fastfile +54 -0
- package/templates/ios/infra/fastlane/metadata/copyright.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/en-US/description.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/en-US/keywords.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/en-US/name.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/en-US/privacy_url.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/en-US/promotional_text.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/en-US/subtitle.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/en-US/support_url.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/description.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/keywords.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/name.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/privacy_url.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/promotional_text.txt +0 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/subtitle.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/fr-FR/support_url.txt +1 -0
- package/templates/ios/infra/fastlane/metadata/primary_category.txt +1 -0
- package/templates/ios/infra/ios.tf +28 -0
- package/templates/ios/ios/ExportOptions.plist +26 -0
- package/templates/ios/ios/apollo-codegen-config.json +16 -0
- package/templates/ios/ios/project.yml +94 -0
- package/templates/ios/ios/{{APP_NAME}}/App.swift +46 -0
- package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/AppIcon.appiconset/AppIcon.png +0 -0
- package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
- package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/Contents.json +6 -0
- package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/LaunchBackground.colorset/Contents.json +38 -0
- package/templates/ios/ios/{{APP_NAME}}/ContentView.swift +17 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AppleNonce.swift +33 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AuthRoot.swift +74 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AuthSession.swift +86 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Auth/LoginScreen.swift +83 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Home/HomeScreen.swift +21 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Home/components/pages/HomePage.swift +40 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/AccountEnvironment.swift +7 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/GraphQL/Onboarding.graphql +16 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingAPI.swift +28 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingClient.swift +26 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingGate.swift +50 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingScreen.swift +69 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/components/pages/FirstNamePage.swift +66 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/components/pages/WelcomePage.swift +37 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ChangelogScreen.swift +25 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ChangelogViewModel.swift +27 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/GraphQL/Settings.graphql +11 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ProfileSettingsScreen.swift +30 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ProfileSettingsViewModel.swift +51 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsAPI.swift +37 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsClient.swift +40 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsHomeScreen.swift +39 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/DeleteAccountButton.swift +30 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/SettingsRow.swift +56 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/SignOutButton.swift +23 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/ChangelogPage.swift +59 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/FeedbackSheet.swift +118 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/ProfileSettingsPage.swift +90 -0
- package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/SettingsHomePage.swift +70 -0
- package/templates/ios/ios/{{APP_NAME}}/Info.plist +14 -0
- package/templates/ios/ios/{{APP_NAME}}/Localizable.xcstrings +1030 -0
- package/templates/ios/ios/{{APP_NAME}}/PrivacyInfo.xcprivacy +122 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/APIClient.swift +23 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/AcceptLanguageInterceptor.swift +20 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/AsyncButton.swift +73 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/AsyncToolbarButton.swift +69 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/ToolbarIconButton.swift +46 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/molecules/EmptyState.swift +132 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/molecules/LabeledInfoRow.swift +29 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/CaptionLabelStyle.swift +45 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/Copyable.swift +32 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/EdgeToEdgeSeparator.swift +14 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/RowLabelStyle.swift +58 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/CursorPage.swift +26 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/DataChanges.swift +19 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/ErrorPresenter.swift +62 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/ErrorReporting.swift +35 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/FeedbackReporting.swift +42 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/FirebaseTokenInterceptor.swift +49 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLClient.swift +54 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLHelpers.swift +128 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLLoggingInterceptor.swift +28 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/LoadGate.swift +30 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Secrets.swift.example +9 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Services/AppSupportGate.swift +50 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Shell/StartupLoadingView.swift +23 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Shell/UpdateRequiredView.swift +24 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/Showcase.swift +13 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/SnapshotCache.swift +78 -0
- package/templates/ios/ios/{{APP_NAME}}/Shared/UITestEnvironment.swift +70 -0
- package/templates/ios/ios/{{APP_NAME}}/{{APP_NAME}}.entitlements +10 -0
- package/templates/ios/ios/{{APP_NAME}}Tests/Onboarding/OnboardingGateTests.swift +38 -0
- package/templates/ios/ios/{{APP_NAME}}Tests/Settings/SettingsViewModelTests.swift +63 -0
- package/templates/ios/ios/{{APP_NAME}}Tests/Support/TestDoubles.swift +35 -0
- package/templates/ios/package.merge.json +10 -0
- package/templates/ios/scripts/apollo-codegen.ts +31 -0
- package/templates/ios/scripts/check-ios-components.ts +72 -0
- package/templates/ios/scripts/check-strings.ts +103 -0
- package/templates/ios/scripts/install-device.sh +29 -0
- package/templates/ios/scripts/testflight.ts +213 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
resource "google_firebaserules_ruleset" "firestore" {
|
|
2
|
+
provider = google-beta
|
|
3
|
+
project = google_project.this.project_id
|
|
4
|
+
|
|
5
|
+
source {
|
|
6
|
+
files {
|
|
7
|
+
name = "firestore.rules"
|
|
8
|
+
content = file("${path.root}/../firestore.rules")
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
depends_on = [google_firestore_database.default]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
resource "google_firebaserules_release" "firestore" {
|
|
16
|
+
provider = google-beta
|
|
17
|
+
project = google_project.this.project_id
|
|
18
|
+
name = "cloud.firestore"
|
|
19
|
+
ruleset_name = "projects/${google_project.this.project_id}/rulesets/${google_firebaserules_ruleset.firestore.name}"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
locals {
|
|
23
|
+
firestore_indexes = jsondecode(file("${path.root}/../firestore.indexes.json")).indexes
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
resource "google_firestore_index" "composite" {
|
|
27
|
+
# Keyed by a stable identifier (collection + fields), not the array position, so
|
|
28
|
+
# adding or removing an index never re-keys — and therefore never destroys and
|
|
29
|
+
# recreates — the others. A position key caused a transient FAILED_PRECONDITION
|
|
30
|
+
# whenever the list changed in the middle.
|
|
31
|
+
for_each = {
|
|
32
|
+
for idx in local.firestore_indexes :
|
|
33
|
+
"${idx.collectionGroup}|${join(",", [for f in idx.fields : "${f.fieldPath}:${try(f.order, try(f.arrayConfig, "NA"))}"])}" => idx
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
provider = google-beta
|
|
37
|
+
project = google_project.this.project_id
|
|
38
|
+
database = google_firestore_database.default.name
|
|
39
|
+
collection = each.value.collectionGroup
|
|
40
|
+
query_scope = each.value.queryScope
|
|
41
|
+
|
|
42
|
+
# A field is indexed either by order (a scalar, ascending or descending) or by
|
|
43
|
+
# array config (`CONTAINS`, what array-contains queries need). Firestore rejects
|
|
44
|
+
# a field that declares neither, so both are read from firestore.indexes.json.
|
|
45
|
+
dynamic "fields" {
|
|
46
|
+
for_each = each.value.fields
|
|
47
|
+
content {
|
|
48
|
+
field_path = fields.value.fieldPath
|
|
49
|
+
order = try(fields.value.order, null)
|
|
50
|
+
array_config = try(fields.value.arrayConfig, null)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
resource "google_service_account" "function" {
|
|
2
|
+
project = google_project.this.project_id
|
|
3
|
+
account_id = "{{APP_SLUG}}-runtime"
|
|
4
|
+
display_name = "{{APP_NAME}} runtime"
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
resource "google_project_iam_member" "function_firestore" {
|
|
8
|
+
project = google_project.this.project_id
|
|
9
|
+
role = "roles/datastore.user"
|
|
10
|
+
member = "serviceAccount:${google_service_account.function.email}"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
# Deleting an account ends with deleting its Firebase Auth user, which needs
|
|
14
|
+
# Firebase Authentication admin rights: without them the wipe stops at its last
|
|
15
|
+
# step with "insufficient permission", every trace of data already gone.
|
|
16
|
+
resource "google_project_iam_member" "function_firebase_auth" {
|
|
17
|
+
project = google_project.this.project_id
|
|
18
|
+
role = "roles/firebaseauth.admin"
|
|
19
|
+
member = "serviceAccount:${google_service_account.function.email}"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Cloud Functions Gen 2 builds run in Cloud Build under the default Compute
|
|
23
|
+
# service account. In a fresh project that role binding is not granted by
|
|
24
|
+
# default, so the first deploy fails. Bound explicitly to make the bootstrap
|
|
25
|
+
# deterministic.
|
|
26
|
+
data "google_compute_default_service_account" "default" {
|
|
27
|
+
project = google_project.this.project_id
|
|
28
|
+
depends_on = [google_project_service.apis]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
resource "google_project_iam_member" "compute_default_builder" {
|
|
32
|
+
project = google_project.this.project_id
|
|
33
|
+
role = "roles/cloudbuild.builds.builder"
|
|
34
|
+
member = "serviceAccount:${data.google_compute_default_service_account.default.email}"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
resource "google_secret_manager_secret_iam_member" "function" {
|
|
38
|
+
for_each = google_secret_manager_secret.this
|
|
39
|
+
project = google_project.this.project_id
|
|
40
|
+
secret_id = each.value.secret_id
|
|
41
|
+
role = "roles/secretmanager.secretAccessor"
|
|
42
|
+
member = "serviceAccount:${google_service_account.function.email}"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
resource "google_storage_bucket" "source" {
|
|
46
|
+
project = google_project.this.project_id
|
|
47
|
+
name = "${google_project.this.project_id}-fn-source"
|
|
48
|
+
location = local.bucket_location
|
|
49
|
+
force_destroy = true
|
|
50
|
+
uniform_bucket_level_access = true
|
|
51
|
+
|
|
52
|
+
depends_on = [google_project_service.apis]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
data "archive_file" "source" {
|
|
56
|
+
type = "zip"
|
|
57
|
+
source_dir = "${path.root}/../.output/server"
|
|
58
|
+
output_path = "${path.root}/../.output/server-source.zip"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
resource "google_storage_bucket_object" "source" {
|
|
62
|
+
name = "source-${data.archive_file.source.output_md5}.zip"
|
|
63
|
+
bucket = google_storage_bucket.source.name
|
|
64
|
+
source = data.archive_file.source.output_path
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
resource "google_cloudfunctions2_function" "server" {
|
|
68
|
+
provider = google-beta
|
|
69
|
+
project = google_project.this.project_id
|
|
70
|
+
name = "{{APP_SLUG}}-server"
|
|
71
|
+
location = var.region
|
|
72
|
+
|
|
73
|
+
build_config {
|
|
74
|
+
runtime = "nodejs22"
|
|
75
|
+
entry_point = "server"
|
|
76
|
+
|
|
77
|
+
source {
|
|
78
|
+
storage_source {
|
|
79
|
+
bucket = google_storage_bucket.source.name
|
|
80
|
+
object = google_storage_bucket_object.source.name
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
service_config {
|
|
86
|
+
# One whole CPU and several requests per instance: the app asks a screen's
|
|
87
|
+
# queries at once, and they mostly wait on Firestore. The globals are
|
|
88
|
+
# per-instance on purpose and the request cache lives on the event, so
|
|
89
|
+
# nothing leaks between requests.
|
|
90
|
+
available_memory = "512Mi"
|
|
91
|
+
available_cpu = "1"
|
|
92
|
+
max_instance_request_concurrency = 8
|
|
93
|
+
# Mirrors `timeoutSeconds` in nitro.config.ts, which Terraform does not read:
|
|
94
|
+
# raise both together.
|
|
95
|
+
timeout_seconds = 60
|
|
96
|
+
max_instance_count = 100
|
|
97
|
+
# Scale to zero on purpose: cold starts are accepted, never a warm instance.
|
|
98
|
+
min_instance_count = 0
|
|
99
|
+
service_account_email = google_service_account.function.email
|
|
100
|
+
|
|
101
|
+
environment_variables = {
|
|
102
|
+
NITRO_FIREBASE_PROJECT_ID = google_project.this.project_id
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
dynamic "secret_environment_variables" {
|
|
106
|
+
for_each = google_secret_manager_secret.this
|
|
107
|
+
content {
|
|
108
|
+
key = "NITRO_${replace(upper(secret_environment_variables.key), "-", "_")}"
|
|
109
|
+
project_id = google_project.this.project_id
|
|
110
|
+
secret = secret_environment_variables.value.secret_id
|
|
111
|
+
version = "latest"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
depends_on = [
|
|
117
|
+
google_project_iam_member.function_firestore,
|
|
118
|
+
google_project_iam_member.compute_default_builder,
|
|
119
|
+
google_secret_manager_secret_iam_member.function,
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Auth is enforced inside the function (server/middleware/auth.ts verifies the
|
|
124
|
+
# Firebase ID token), so the underlying Cloud Run service is publicly invokable.
|
|
125
|
+
resource "google_cloud_run_service_iam_member" "invoker" {
|
|
126
|
+
project = google_project.this.project_id
|
|
127
|
+
location = var.region
|
|
128
|
+
service = google_cloudfunctions2_function.server.name
|
|
129
|
+
role = "roles/run.invoker"
|
|
130
|
+
member = "allUsers"
|
|
131
|
+
|
|
132
|
+
depends_on = [google_org_policy_policy.allow_public_iam]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Every deploy builds a container image into this repository, which Cloud
|
|
136
|
+
# Functions creates on its first deploy and never prunes. Any image is rebuilt
|
|
137
|
+
# from its commit, so only the running one and the one before are kept.
|
|
138
|
+
import {
|
|
139
|
+
to = google_artifact_registry_repository.functions
|
|
140
|
+
id = "projects/${var.project_id}/locations/${var.region}/repositories/gcf-artifacts"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
resource "google_artifact_registry_repository" "functions" {
|
|
144
|
+
project = google_project.this.project_id
|
|
145
|
+
location = var.region
|
|
146
|
+
repository_id = "gcf-artifacts"
|
|
147
|
+
format = "DOCKER"
|
|
148
|
+
description = "This repository is created and used by Cloud Functions for storing function docker images."
|
|
149
|
+
|
|
150
|
+
cleanup_policy_dry_run = false
|
|
151
|
+
|
|
152
|
+
cleanup_policies {
|
|
153
|
+
id = "delete-every-version"
|
|
154
|
+
action = "DELETE"
|
|
155
|
+
condition {
|
|
156
|
+
tag_state = "ANY"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
cleanup_policies {
|
|
161
|
+
id = "keep-the-two-latest"
|
|
162
|
+
action = "KEEP"
|
|
163
|
+
most_recent_versions {
|
|
164
|
+
keep_count = 2
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
lifecycle {
|
|
169
|
+
ignore_changes = [labels]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
provider "google" {
|
|
2
|
+
project = var.project_id
|
|
3
|
+
region = var.region
|
|
4
|
+
billing_project = local.quota_project
|
|
5
|
+
user_project_override = true
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
provider "google-beta" {
|
|
9
|
+
project = var.project_id
|
|
10
|
+
region = var.region
|
|
11
|
+
billing_project = local.quota_project
|
|
12
|
+
user_project_override = true
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
locals {
|
|
16
|
+
quota_project = var.quota_project_id != "" ? var.quota_project_id : var.project_id
|
|
17
|
+
|
|
18
|
+
# Cloud Storage multi-region IDs are uppercase ("EU", "US", "ASIA"),
|
|
19
|
+
# while Firestore uses lowercase ("eur3", "nam5"). Map between the two
|
|
20
|
+
# so buckets can be co-located with Firestore without a separate var.
|
|
21
|
+
bucket_location = lookup(
|
|
22
|
+
{ eur3 = "EU", nam5 = "US", asia1 = "ASIA" },
|
|
23
|
+
var.firestore_location,
|
|
24
|
+
var.firestore_location,
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# The organisation's read-only observer account (one for every project, living
|
|
2
|
+
# in polyforms-billing) is granted viewer roles here, so the scheduled routines
|
|
3
|
+
# read logs, metrics, scheduler state and Firestore without a personal login.
|
|
4
|
+
# Nothing it holds can change the project.
|
|
5
|
+
resource "google_project_iam_member" "observer" {
|
|
6
|
+
for_each = var.observer_service_account == "" ? toset([]) : toset([
|
|
7
|
+
"roles/logging.viewer",
|
|
8
|
+
"roles/monitoring.viewer",
|
|
9
|
+
"roles/cloudscheduler.viewer",
|
|
10
|
+
"roles/cloudfunctions.viewer",
|
|
11
|
+
"roles/run.viewer",
|
|
12
|
+
"roles/datastore.viewer",
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
project = google_project.this.project_id
|
|
16
|
+
role = each.value
|
|
17
|
+
member = "serviceAccount:${var.observer_service_account}"
|
|
18
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
output "project_id" {
|
|
2
|
+
value = google_project.this.project_id
|
|
3
|
+
description = "GCP project id"
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
output "function_url" {
|
|
7
|
+
value = google_cloudfunctions2_function.server.service_config[0].uri
|
|
8
|
+
description = "HTTPS URL of the Cloud Function (Gen 2)"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
output "admin_token" {
|
|
12
|
+
value = local.admin_token_value
|
|
13
|
+
sensitive = true
|
|
14
|
+
description = "Bearer token for /admin/* routes (used by the post-deploy migration step)"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
output "tfstate_bucket" {
|
|
18
|
+
value = google_storage_bucket.tfstate.name
|
|
19
|
+
description = "GCS bucket holding the Terraform state. Referenced by infra/backend.tf."
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
output "deploy_sa_email" {
|
|
23
|
+
value = google_service_account.deploy.email
|
|
24
|
+
description = "Service account impersonated by the GitHub Actions deploy workflow (GitHub secret GCP_DEPLOY_SA)"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
output "wif_provider" {
|
|
28
|
+
value = google_iam_workload_identity_pool_provider.github.name
|
|
29
|
+
description = "Full resource name of the WIF provider (GitHub secret GCP_WIF_PROVIDER)"
|
|
30
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
resource "google_project" "this" {
|
|
2
|
+
name = var.project_name
|
|
3
|
+
project_id = var.project_id
|
|
4
|
+
org_id = var.org_id
|
|
5
|
+
folder_id = var.folder_id
|
|
6
|
+
billing_account = var.billing_account_id
|
|
7
|
+
|
|
8
|
+
labels = {
|
|
9
|
+
app = "{{APP_SLUG}}"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
resource "google_project_service" "apis" {
|
|
14
|
+
for_each = toset([
|
|
15
|
+
"firebase.googleapis.com",
|
|
16
|
+
"firestore.googleapis.com",
|
|
17
|
+
"identitytoolkit.googleapis.com",
|
|
18
|
+
"cloudfunctions.googleapis.com",
|
|
19
|
+
"cloudbuild.googleapis.com",
|
|
20
|
+
"compute.googleapis.com",
|
|
21
|
+
"run.googleapis.com",
|
|
22
|
+
"artifactregistry.googleapis.com",
|
|
23
|
+
"eventarc.googleapis.com",
|
|
24
|
+
"storage.googleapis.com",
|
|
25
|
+
"secretmanager.googleapis.com",
|
|
26
|
+
"iam.googleapis.com",
|
|
27
|
+
"iamcredentials.googleapis.com",
|
|
28
|
+
"serviceusage.googleapis.com",
|
|
29
|
+
"cloudresourcemanager.googleapis.com",
|
|
30
|
+
"firebaserules.googleapis.com",
|
|
31
|
+
"orgpolicy.googleapis.com",
|
|
32
|
+
"cloudscheduler.googleapis.com",
|
|
33
|
+
"logging.googleapis.com",
|
|
34
|
+
"monitoring.googleapis.com",
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
project = google_project.this.project_id
|
|
38
|
+
service = each.value
|
|
39
|
+
disable_on_destroy = false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Many GCP organizations enforce iam.allowedPolicyMemberDomains, which blocks
|
|
43
|
+
# IAM bindings to allUsers. The Cloud Run service must be publicly invokable
|
|
44
|
+
# because auth is enforced inside the function (Firebase ID token verification).
|
|
45
|
+
# The constraint is overridden at the project scope, never org-wide.
|
|
46
|
+
resource "google_org_policy_policy" "allow_public_iam" {
|
|
47
|
+
parent = "projects/${google_project.this.project_id}"
|
|
48
|
+
name = "projects/${google_project.this.project_id}/policies/iam.allowedPolicyMemberDomains"
|
|
49
|
+
|
|
50
|
+
spec {
|
|
51
|
+
rules {
|
|
52
|
+
allow_all = "TRUE"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
depends_on = [google_project_service.apis]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Org policy changes take about a minute to propagate before downstream IAM
|
|
60
|
+
# bindings succeed. Without this gate the first apply fails and needs a rerun.
|
|
61
|
+
resource "time_sleep" "wait_for_org_policy" {
|
|
62
|
+
depends_on = [google_org_policy_policy.allow_public_iam]
|
|
63
|
+
create_duration = "90s"
|
|
64
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# One Cloud Scheduler job per scheduled route of the backend. A route called
|
|
2
|
+
# here is idempotent (a retry neither pays twice nor notifies twice), answers
|
|
3
|
+
# quickly, does its work in bounded batches, and is gated by the admin token,
|
|
4
|
+
# exactly like the migration step of the Deploy workflow.
|
|
5
|
+
#
|
|
6
|
+
# resource "google_cloud_scheduler_job" "example" {
|
|
7
|
+
# project = google_project.this.project_id
|
|
8
|
+
# region = var.region
|
|
9
|
+
# name = "example"
|
|
10
|
+
# schedule = "0 6 * * *"
|
|
11
|
+
# time_zone = "Europe/Paris"
|
|
12
|
+
#
|
|
13
|
+
# attempt_deadline = "60s"
|
|
14
|
+
#
|
|
15
|
+
# retry_config {
|
|
16
|
+
# retry_count = 1
|
|
17
|
+
# }
|
|
18
|
+
#
|
|
19
|
+
# http_target {
|
|
20
|
+
# http_method = "POST"
|
|
21
|
+
# uri = "${google_cloudfunctions2_function.server.service_config[0].uri}/admin/example"
|
|
22
|
+
# headers = {
|
|
23
|
+
# Authorization = "Bearer ${local.admin_token_value}"
|
|
24
|
+
# }
|
|
25
|
+
# }
|
|
26
|
+
#
|
|
27
|
+
# depends_on = [google_project_service.apis]
|
|
28
|
+
# }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
resource "random_password" "admin_token" {
|
|
2
|
+
count = var.admin_token == null ? 1 : 0
|
|
3
|
+
length = 48
|
|
4
|
+
special = false
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
locals {
|
|
8
|
+
admin_token_value = var.admin_token != null ? var.admin_token : random_password.admin_token[0].result
|
|
9
|
+
|
|
10
|
+
# Every secret the function reads, by Secret Manager id. The id becomes the
|
|
11
|
+
# NITRO_* variable name (function.tf). Generated material uses `random_bytes`,
|
|
12
|
+
# never `random_id`: the latter makes the value its own id, which Terraform
|
|
13
|
+
# prints into the build log of a public repository.
|
|
14
|
+
secret_values = {
|
|
15
|
+
admin-token = local.admin_token_value
|
|
16
|
+
sentry-dsn = var.sentry_dsn
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Secret Manager rejects empty payloads, so optional secrets are skipped when
|
|
20
|
+
# blank. Iterating a map of sensitive values would propagate sensitivity into
|
|
21
|
+
# for_each keys, which Terraform forbids, hence nonsensitive().
|
|
22
|
+
secret_ids = toset(compact([
|
|
23
|
+
"admin-token",
|
|
24
|
+
nonsensitive(var.sentry_dsn) != "" ? "sentry-dsn" : "",
|
|
25
|
+
]))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
resource "google_secret_manager_secret" "this" {
|
|
29
|
+
for_each = local.secret_ids
|
|
30
|
+
project = google_project.this.project_id
|
|
31
|
+
secret_id = each.value
|
|
32
|
+
|
|
33
|
+
replication {
|
|
34
|
+
auto {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
depends_on = [google_project_service.apis]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
resource "google_secret_manager_secret_version" "this" {
|
|
41
|
+
for_each = local.secret_ids
|
|
42
|
+
secret = google_secret_manager_secret.this[each.value].id
|
|
43
|
+
secret_data = local.secret_values[each.value]
|
|
44
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
resource "google_storage_bucket" "tfstate" {
|
|
2
|
+
project = google_project.this.project_id
|
|
3
|
+
name = "${google_project.this.project_id}-tfstate"
|
|
4
|
+
location = local.bucket_location
|
|
5
|
+
force_destroy = false
|
|
6
|
+
uniform_bucket_level_access = true
|
|
7
|
+
|
|
8
|
+
versioning {
|
|
9
|
+
enabled = true
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
depends_on = [google_project_service.apis]
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
project_id = "{{APP_SLUG}}-polyforms"
|
|
2
|
+
project_name = "{{APP_NAME}}"
|
|
3
|
+
org_id = "123456789012" # OR set folder_id instead
|
|
4
|
+
billing_account_id = "AAAA-BBBB-CCCC-DDDD"
|
|
5
|
+
region = "europe-west3"
|
|
6
|
+
firestore_location = "eur3"
|
|
7
|
+
|
|
8
|
+
# Apple Sign-In (download the .p8 and the ids from https://developer.apple.com/account)
|
|
9
|
+
apple_team_id = "ABCD123456"
|
|
10
|
+
apple_services_id = "{{BUNDLE_ID}}.signin"
|
|
11
|
+
apple_key_id = "KEY1234567"
|
|
12
|
+
apple_private_key_path = "./AuthKey_KEY1234567.p8"
|
|
13
|
+
|
|
14
|
+
# Backend secrets
|
|
15
|
+
sentry_dsn = "" # optional
|
|
16
|
+
# admin_token = "..." # optional, auto-generated if omitted
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
variable "project_id" {
|
|
2
|
+
description = "Globally unique GCP project id ({{APP_SLUG}}-polyforms)"
|
|
3
|
+
type = string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
variable "project_name" {
|
|
7
|
+
description = "Human-readable project name shown in the GCP console"
|
|
8
|
+
type = string
|
|
9
|
+
default = "{{APP_NAME}}"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
variable "org_id" {
|
|
13
|
+
description = "GCP organization id (mutually exclusive with folder_id)"
|
|
14
|
+
type = string
|
|
15
|
+
default = null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
variable "folder_id" {
|
|
19
|
+
description = "GCP folder id (mutually exclusive with org_id)"
|
|
20
|
+
type = string
|
|
21
|
+
default = null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
variable "billing_account_id" {
|
|
25
|
+
description = "GCP billing account id (AAAA-BBBB-CCCC-DDDD)"
|
|
26
|
+
type = string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
variable "region" {
|
|
30
|
+
description = "Region for the Cloud Function (Gen 2)"
|
|
31
|
+
type = string
|
|
32
|
+
default = "europe-west3"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
variable "firestore_location" {
|
|
36
|
+
description = "Firestore multi-region or region (eur3, europe-west3)"
|
|
37
|
+
type = string
|
|
38
|
+
default = "eur3"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
variable "github_repo" {
|
|
42
|
+
description = "GitHub repository (owner/name) allowed to deploy via Workload Identity Federation"
|
|
43
|
+
type = string
|
|
44
|
+
default = "{{GITHUB_REPO}}"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
variable "observer_service_account" {
|
|
48
|
+
description = "Email of the organisation's read-only observer account (lives in polyforms-billing). Blank grants nothing."
|
|
49
|
+
type = string
|
|
50
|
+
default = ""
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
variable "quota_project_id" {
|
|
54
|
+
description = <<-EOT
|
|
55
|
+
Project the provider attributes API quota to (user_project_override).
|
|
56
|
+
|
|
57
|
+
Defaults to the project being created, which is right in steady state and
|
|
58
|
+
impossible on the very first apply: the billing pre-flight calls
|
|
59
|
+
cloudbilling.googleapis.com against a project that does not exist yet.
|
|
60
|
+
Point this at an existing project for the bootstrap run, then drop it.
|
|
61
|
+
EOT
|
|
62
|
+
type = string
|
|
63
|
+
default = ""
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Apple Sign-In. All blank leaves the provider unconfigured (auth.tf).
|
|
67
|
+
variable "apple_team_id" {
|
|
68
|
+
description = "Apple Developer Team ID (10 characters)"
|
|
69
|
+
type = string
|
|
70
|
+
default = ""
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
variable "apple_services_id" {
|
|
74
|
+
description = "Apple Sign-In Services ID (the OAuth client_id)"
|
|
75
|
+
type = string
|
|
76
|
+
default = ""
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
variable "apple_key_id" {
|
|
80
|
+
description = "Apple private key ID (10 characters, matches the .p8 filename)"
|
|
81
|
+
type = string
|
|
82
|
+
default = ""
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
variable "apple_private_key_path" {
|
|
86
|
+
description = "Path to the AuthKey_XXXXXXXXXX.p8 file from Apple Developer"
|
|
87
|
+
type = string
|
|
88
|
+
default = ""
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Backend secrets
|
|
92
|
+
variable "sentry_dsn" {
|
|
93
|
+
description = "Sentry DSN of the {{APP_SLUG}}-node project (empty disables Sentry). Only ever takes effect in the deployed function."
|
|
94
|
+
type = string
|
|
95
|
+
sensitive = true
|
|
96
|
+
default = ""
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
variable "admin_token" {
|
|
100
|
+
description = "Bearer token guarding /admin/* routes. Auto-generated if null."
|
|
101
|
+
type = string
|
|
102
|
+
sensitive = true
|
|
103
|
+
default = null
|
|
104
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
terraform {
|
|
2
|
+
required_version = ">= 1.6"
|
|
3
|
+
|
|
4
|
+
required_providers {
|
|
5
|
+
google = {
|
|
6
|
+
source = "hashicorp/google"
|
|
7
|
+
version = "~> 8.0"
|
|
8
|
+
}
|
|
9
|
+
google-beta = {
|
|
10
|
+
source = "hashicorp/google-beta"
|
|
11
|
+
version = "~> 8.0"
|
|
12
|
+
}
|
|
13
|
+
archive = {
|
|
14
|
+
source = "hashicorp/archive"
|
|
15
|
+
version = "~> 2.4"
|
|
16
|
+
}
|
|
17
|
+
local = {
|
|
18
|
+
source = "hashicorp/local"
|
|
19
|
+
version = "~> 2.5"
|
|
20
|
+
}
|
|
21
|
+
random = {
|
|
22
|
+
source = "hashicorp/random"
|
|
23
|
+
version = "~> 3.6"
|
|
24
|
+
}
|
|
25
|
+
time = {
|
|
26
|
+
source = "hashicorp/time"
|
|
27
|
+
version = "~> 0.14"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export default defineNitroConfig({
|
|
2
|
+
compatibilityDate: '2026-09-14',
|
|
3
|
+
experimental: { asyncContext: true },
|
|
4
|
+
srcDir: 'server',
|
|
5
|
+
ignore: ['**/*.test.ts'],
|
|
6
|
+
preset: 'firebase',
|
|
7
|
+
firebase: {
|
|
8
|
+
gen: 2,
|
|
9
|
+
nodeVersion: '22',
|
|
10
|
+
httpsOptions: {
|
|
11
|
+
region: 'europe-west3',
|
|
12
|
+
memory: '512MiB',
|
|
13
|
+
// Only `firebase deploy` reads this block. The deploy goes through
|
|
14
|
+
// Terraform, so the ceiling that applies is `timeout_seconds` in
|
|
15
|
+
// infra/function.tf; the two are kept equal so that neither misleads.
|
|
16
|
+
timeoutSeconds: 60,
|
|
17
|
+
concurrency: 80,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
// Rollup minifies the whole backend into one file. The maps are uploaded to
|
|
21
|
+
// Sentry at deploy time and stay out of the deployed bundle.
|
|
22
|
+
sourceMap: true,
|
|
23
|
+
rollupConfig: {
|
|
24
|
+
treeshake: {
|
|
25
|
+
moduleSideEffects: (id) => id.includes('/graphql/') || id.includes('node_modules'),
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
runtimeConfig: {
|
|
29
|
+
adminToken: '',
|
|
30
|
+
sentryDsn: '',
|
|
31
|
+
// Baked in at build time by the deploy workflow (the git SHA), so Sentry can
|
|
32
|
+
// tell which deploy an error comes from and pick the matching source maps.
|
|
33
|
+
sentryRelease: process.env.SENTRY_RELEASE ?? '',
|
|
34
|
+
devUserId: '',
|
|
35
|
+
// The project the function runs in, set by Terraform.
|
|
36
|
+
firebaseProjectId: '',
|
|
37
|
+
// Dev only: the origin the simulator reaches this server at.
|
|
38
|
+
publicBaseUrl: 'http://127.0.0.1:3000',
|
|
39
|
+
},
|
|
40
|
+
})
|