@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.
Files changed (263) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +26 -2
  3. package/bin/stack.ts +109 -0
  4. package/hooks/shutdown-simulators-after-push.sh +19 -0
  5. package/package.json +40 -4
  6. package/routines/daily-health.md +78 -0
  7. package/routines/deprecations.md +23 -0
  8. package/routines/sentry-autofix.md +76 -0
  9. package/routines/stack-upgrade.md +45 -0
  10. package/rules/00-collaboration.md +81 -0
  11. package/rules/01-workflow.md +81 -0
  12. package/rules/02-commits.md +71 -0
  13. package/rules/03-code-review.md +25 -0
  14. package/rules/04-push.md +75 -0
  15. package/rules/05-release.md +40 -0
  16. package/rules/06-changelog.md +48 -0
  17. package/rules/07-appstore.md +77 -0
  18. package/rules/08-copy.md +52 -0
  19. package/rules/09-backend.md +132 -0
  20. package/rules/10-ios.md +206 -0
  21. package/rules/11-ops.md +114 -0
  22. package/rules/12-ci.md +32 -0
  23. package/rules/13-ai.md +71 -0
  24. package/rules/14-infra.md +90 -0
  25. package/rules/15-tooling.md +81 -0
  26. package/rules/16-routines.md +69 -0
  27. package/rules/17-landing.md +76 -0
  28. package/skills/appstore/SKILL.md +53 -0
  29. package/skills/domain/SKILL.md +136 -0
  30. package/skills/graphql/SKILL.md +83 -0
  31. package/skills/ios-feature/SKILL.md +94 -0
  32. package/skills/stack-install/SKILL.md +35 -0
  33. package/skills/stack-upgrade/SKILL.md +27 -0
  34. package/src/catalogue.ts +33 -0
  35. package/src/config.ts +20 -0
  36. package/src/init.ts +63 -0
  37. package/src/migrations.ts +72 -0
  38. package/src/migrations.unit.test.ts +44 -0
  39. package/src/package.ts +8 -0
  40. package/src/render.ts +13 -0
  41. package/src/render.unit.test.ts +17 -0
  42. package/src/sync.int.test.ts +103 -0
  43. package/src/sync.ts +161 -0
  44. package/src/templates.ts +121 -0
  45. package/src/validate-content.ts +65 -0
  46. package/templates/backend/.env.example +10 -0
  47. package/templates/backend/.github/workflows/deploy.yml +151 -0
  48. package/templates/backend/.github/workflows/schema.yml +59 -0
  49. package/templates/backend/.github/workflows/test-feat.yml +40 -0
  50. package/templates/backend/.github/workflows/test-int.yml +40 -0
  51. package/templates/backend/.github/workflows/test-unit.yml +64 -0
  52. package/templates/backend/docs/api-evolution.md +67 -0
  53. package/templates/backend/firebase.json +21 -0
  54. package/templates/backend/firestore.indexes.json +4 -0
  55. package/templates/backend/firestore.rules +11 -0
  56. package/templates/backend/infra/auth.tf +54 -0
  57. package/templates/backend/infra/backend.tf +6 -0
  58. package/templates/backend/infra/cicd.tf +63 -0
  59. package/templates/backend/infra/firebase.tf +26 -0
  60. package/templates/backend/infra/firestore.tf +53 -0
  61. package/templates/backend/infra/function.tf +171 -0
  62. package/templates/backend/infra/main.tf +26 -0
  63. package/templates/backend/infra/observer.tf +18 -0
  64. package/templates/backend/infra/outputs.tf +30 -0
  65. package/templates/backend/infra/project.tf +64 -0
  66. package/templates/backend/infra/scheduler.tf +28 -0
  67. package/templates/backend/infra/secrets.tf +44 -0
  68. package/templates/backend/infra/state.tf +13 -0
  69. package/templates/backend/infra/terraform.tfvars.example +16 -0
  70. package/templates/backend/infra/variables.tf +104 -0
  71. package/templates/backend/infra/versions.tf +30 -0
  72. package/templates/backend/nitro.config.ts +40 -0
  73. package/templates/backend/package.json +51 -0
  74. package/templates/backend/scripts/deprecations/inspector-rule.ts +29 -0
  75. package/templates/backend/scripts/deprecations/status.ts +50 -0
  76. package/templates/backend/scripts/deprecations/status.unit.test.ts +57 -0
  77. package/templates/backend/scripts/deprecations/usage.ts +79 -0
  78. package/templates/backend/scripts/deprecations/usage.unit.test.ts +64 -0
  79. package/templates/backend/scripts/deprecations.ts +224 -0
  80. package/templates/backend/scripts/export-graphql-schema.ts +11 -0
  81. package/templates/backend/scripts/generate-changelog-asset.ts +27 -0
  82. package/templates/backend/scripts/install-terraform.sh +61 -0
  83. package/templates/backend/scripts/tf +11 -0
  84. package/templates/backend/server/domain/changelog/business-rules.ts +60 -0
  85. package/templates/backend/server/domain/changelog/business-rules.unit.test.ts +88 -0
  86. package/templates/backend/server/domain/changelog/infrastructure/graphql/queries.ts +17 -0
  87. package/templates/backend/server/domain/changelog/infrastructure/graphql/types.ts +23 -0
  88. package/templates/backend/server/domain/changelog/primitives.ts +19 -0
  89. package/templates/backend/server/domain/changelog/query.ts +13 -0
  90. package/templates/backend/server/domain/changelog/query.unit.test.ts +19 -0
  91. package/templates/backend/server/domain/changelog/types.ts +9 -0
  92. package/templates/backend/server/domain/shared/graphql/builder.ts +52 -0
  93. package/templates/backend/server/domain/shared/graphql/errors.ts +19 -0
  94. package/templates/backend/server/domain/shared/graphql/scalars.ts +51 -0
  95. package/templates/backend/server/domain/shared/graphql/schema.ts +21 -0
  96. package/templates/backend/server/domain/shared/language.ts +26 -0
  97. package/templates/backend/server/domain/shared/page.ts +25 -0
  98. package/templates/backend/server/domain/shared/page.unit.test.ts +51 -0
  99. package/templates/backend/server/domain/shared/primitives.ts +50 -0
  100. package/templates/backend/server/domain/shared/primitives.unit.test.ts +25 -0
  101. package/templates/backend/server/domain/shared/types.ts +14 -0
  102. package/templates/backend/server/domain/user/command.ts +16 -0
  103. package/templates/backend/server/domain/user/infrastructure/graphql/inputs.ts +12 -0
  104. package/templates/backend/server/domain/user/infrastructure/graphql/mutations.ts +40 -0
  105. package/templates/backend/server/domain/user/infrastructure/graphql/queries.ts +13 -0
  106. package/templates/backend/server/domain/user/infrastructure/graphql/types.ts +35 -0
  107. package/templates/backend/server/domain/user/infrastructure/repository.ts +59 -0
  108. package/templates/backend/server/domain/user/mutations.feat.test.ts +42 -0
  109. package/templates/backend/server/domain/user/query.int.test.ts +49 -0
  110. package/templates/backend/server/domain/user/query.ts +34 -0
  111. package/templates/backend/server/domain/user/types.ts +20 -0
  112. package/templates/backend/server/domain/user/use-case.ts +18 -0
  113. package/templates/backend/server/middleware/auth.ts +54 -0
  114. package/templates/backend/server/plugins/00-sentry.ts +74 -0
  115. package/templates/backend/server/plugins/01-request-log.ts +28 -0
  116. package/templates/backend/server/plugins/02-graphql.ts +69 -0
  117. package/templates/backend/server/plugins/03-boot-time.ts +21 -0
  118. package/templates/backend/server/routes/admin/migrate.post.ts +9 -0
  119. package/templates/backend/server/routes/app-config.get.ts +15 -0
  120. package/templates/backend/server/routes/graphql.ts +52 -0
  121. package/templates/backend/server/system/app-support.ts +17 -0
  122. package/templates/backend/server/system/config/index.ts +18 -0
  123. package/templates/backend/server/system/config/primitives.ts +20 -0
  124. package/templates/backend/server/system/config/primitives.unit.test.ts +13 -0
  125. package/templates/backend/server/system/config/types.ts +8 -0
  126. package/templates/backend/server/system/firebase.ts +19 -0
  127. package/templates/backend/server/system/identity.ts +18 -0
  128. package/templates/backend/server/system/logger.ts +119 -0
  129. package/templates/backend/server/system/logger.unit.test.ts +109 -0
  130. package/templates/backend/server/system/migration/migrations/index.ts +6 -0
  131. package/templates/backend/server/system/migration/primitives.ts +18 -0
  132. package/templates/backend/server/system/migration/runner.ts +52 -0
  133. package/templates/backend/server/system/migration/types.ts +16 -0
  134. package/templates/backend/server/system/read-meter.ts +100 -0
  135. package/templates/backend/server/system/read-meter.unit.test.ts +97 -0
  136. package/templates/backend/server/system/request-cache.ts +139 -0
  137. package/templates/backend/server/system/request-cache.unit.test.ts +166 -0
  138. package/templates/backend/server/system/secret-box.ts +49 -0
  139. package/templates/backend/server/system/secret-box.unit.test.ts +46 -0
  140. package/templates/backend/server/test/bdd.ts +12 -0
  141. package/templates/backend/server/test/fake-firestore.ts +483 -0
  142. package/templates/backend/server/utils/apollo.ts +13 -0
  143. package/templates/backend/server/utils/fetch.ts +5 -0
  144. package/templates/backend/server/utils/firestore.ts +91 -0
  145. package/templates/backend/server/utils/input.ts +36 -0
  146. package/templates/backend/server/utils/input.unit.test.ts +43 -0
  147. package/templates/backend/server/utils/slug.ts +21 -0
  148. package/templates/backend/tsconfig.json +16 -0
  149. package/templates/base/.github/workflows/renovate.yml +17 -0
  150. package/templates/base/CHANGELOG.fr.md +3 -0
  151. package/templates/base/CHANGELOG.md +7 -0
  152. package/templates/base/CLAUDE.md +34 -0
  153. package/templates/base/README.md +12 -0
  154. package/templates/base/biome.json +47 -0
  155. package/templates/base/bunfig.toml +2 -0
  156. package/templates/base/docs/adr/0001-built-on-the-polyforms-stack.md +21 -0
  157. package/templates/base/docs/data-recovery.md +116 -0
  158. package/templates/base/docs/ideas.md +4 -0
  159. package/templates/base/project-context.md +43 -0
  160. package/templates/base/renovate.json +37 -0
  161. package/templates/ios/.github/workflows/build-ios.yml +168 -0
  162. package/templates/ios/.github/workflows/ios-upload.yml +308 -0
  163. package/templates/ios/.github/workflows/release-ios.yml +178 -0
  164. package/templates/ios/.github/workflows/testflight-ios.yml +57 -0
  165. package/templates/ios/infra/Gemfile +5 -0
  166. package/templates/ios/infra/fastlane/Appfile +1 -0
  167. package/templates/ios/infra/fastlane/Fastfile +54 -0
  168. package/templates/ios/infra/fastlane/metadata/copyright.txt +1 -0
  169. package/templates/ios/infra/fastlane/metadata/en-US/description.txt +0 -0
  170. package/templates/ios/infra/fastlane/metadata/en-US/keywords.txt +0 -0
  171. package/templates/ios/infra/fastlane/metadata/en-US/name.txt +1 -0
  172. package/templates/ios/infra/fastlane/metadata/en-US/privacy_url.txt +1 -0
  173. package/templates/ios/infra/fastlane/metadata/en-US/promotional_text.txt +0 -0
  174. package/templates/ios/infra/fastlane/metadata/en-US/subtitle.txt +1 -0
  175. package/templates/ios/infra/fastlane/metadata/en-US/support_url.txt +1 -0
  176. package/templates/ios/infra/fastlane/metadata/fr-FR/description.txt +0 -0
  177. package/templates/ios/infra/fastlane/metadata/fr-FR/keywords.txt +0 -0
  178. package/templates/ios/infra/fastlane/metadata/fr-FR/name.txt +1 -0
  179. package/templates/ios/infra/fastlane/metadata/fr-FR/privacy_url.txt +1 -0
  180. package/templates/ios/infra/fastlane/metadata/fr-FR/promotional_text.txt +0 -0
  181. package/templates/ios/infra/fastlane/metadata/fr-FR/subtitle.txt +1 -0
  182. package/templates/ios/infra/fastlane/metadata/fr-FR/support_url.txt +1 -0
  183. package/templates/ios/infra/fastlane/metadata/primary_category.txt +1 -0
  184. package/templates/ios/infra/ios.tf +28 -0
  185. package/templates/ios/ios/ExportOptions.plist +26 -0
  186. package/templates/ios/ios/apollo-codegen-config.json +16 -0
  187. package/templates/ios/ios/project.yml +94 -0
  188. package/templates/ios/ios/{{APP_NAME}}/App.swift +46 -0
  189. package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/AppIcon.appiconset/AppIcon.png +0 -0
  190. package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
  191. package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/Contents.json +6 -0
  192. package/templates/ios/ios/{{APP_NAME}}/Assets.xcassets/LaunchBackground.colorset/Contents.json +38 -0
  193. package/templates/ios/ios/{{APP_NAME}}/ContentView.swift +17 -0
  194. package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AppleNonce.swift +33 -0
  195. package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AuthRoot.swift +74 -0
  196. package/templates/ios/ios/{{APP_NAME}}/Features/Auth/AuthSession.swift +86 -0
  197. package/templates/ios/ios/{{APP_NAME}}/Features/Auth/LoginScreen.swift +83 -0
  198. package/templates/ios/ios/{{APP_NAME}}/Features/Home/HomeScreen.swift +21 -0
  199. package/templates/ios/ios/{{APP_NAME}}/Features/Home/components/pages/HomePage.swift +40 -0
  200. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/AccountEnvironment.swift +7 -0
  201. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/GraphQL/Onboarding.graphql +16 -0
  202. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingAPI.swift +28 -0
  203. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingClient.swift +26 -0
  204. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingGate.swift +50 -0
  205. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/OnboardingScreen.swift +69 -0
  206. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/components/pages/FirstNamePage.swift +66 -0
  207. package/templates/ios/ios/{{APP_NAME}}/Features/Onboarding/components/pages/WelcomePage.swift +37 -0
  208. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ChangelogScreen.swift +25 -0
  209. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ChangelogViewModel.swift +27 -0
  210. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/GraphQL/Settings.graphql +11 -0
  211. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ProfileSettingsScreen.swift +30 -0
  212. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/ProfileSettingsViewModel.swift +51 -0
  213. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsAPI.swift +37 -0
  214. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsClient.swift +40 -0
  215. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/SettingsHomeScreen.swift +39 -0
  216. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/DeleteAccountButton.swift +30 -0
  217. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/SettingsRow.swift +56 -0
  218. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/organisms/SignOutButton.swift +23 -0
  219. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/ChangelogPage.swift +59 -0
  220. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/FeedbackSheet.swift +118 -0
  221. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/ProfileSettingsPage.swift +90 -0
  222. package/templates/ios/ios/{{APP_NAME}}/Features/Settings/components/pages/SettingsHomePage.swift +70 -0
  223. package/templates/ios/ios/{{APP_NAME}}/Info.plist +14 -0
  224. package/templates/ios/ios/{{APP_NAME}}/Localizable.xcstrings +1030 -0
  225. package/templates/ios/ios/{{APP_NAME}}/PrivacyInfo.xcprivacy +122 -0
  226. package/templates/ios/ios/{{APP_NAME}}/Shared/APIClient.swift +23 -0
  227. package/templates/ios/ios/{{APP_NAME}}/Shared/AcceptLanguageInterceptor.swift +20 -0
  228. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/AsyncButton.swift +73 -0
  229. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/AsyncToolbarButton.swift +69 -0
  230. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/atoms/ToolbarIconButton.swift +46 -0
  231. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/molecules/EmptyState.swift +132 -0
  232. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/molecules/LabeledInfoRow.swift +29 -0
  233. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/CaptionLabelStyle.swift +45 -0
  234. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/Copyable.swift +32 -0
  235. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/EdgeToEdgeSeparator.swift +14 -0
  236. package/templates/ios/ios/{{APP_NAME}}/Shared/Components/styles/RowLabelStyle.swift +58 -0
  237. package/templates/ios/ios/{{APP_NAME}}/Shared/CursorPage.swift +26 -0
  238. package/templates/ios/ios/{{APP_NAME}}/Shared/DataChanges.swift +19 -0
  239. package/templates/ios/ios/{{APP_NAME}}/Shared/ErrorPresenter.swift +62 -0
  240. package/templates/ios/ios/{{APP_NAME}}/Shared/ErrorReporting.swift +35 -0
  241. package/templates/ios/ios/{{APP_NAME}}/Shared/FeedbackReporting.swift +42 -0
  242. package/templates/ios/ios/{{APP_NAME}}/Shared/FirebaseTokenInterceptor.swift +49 -0
  243. package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLClient.swift +54 -0
  244. package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLHelpers.swift +128 -0
  245. package/templates/ios/ios/{{APP_NAME}}/Shared/GraphQLLoggingInterceptor.swift +28 -0
  246. package/templates/ios/ios/{{APP_NAME}}/Shared/LoadGate.swift +30 -0
  247. package/templates/ios/ios/{{APP_NAME}}/Shared/Secrets.swift.example +9 -0
  248. package/templates/ios/ios/{{APP_NAME}}/Shared/Services/AppSupportGate.swift +50 -0
  249. package/templates/ios/ios/{{APP_NAME}}/Shared/Shell/StartupLoadingView.swift +23 -0
  250. package/templates/ios/ios/{{APP_NAME}}/Shared/Shell/UpdateRequiredView.swift +24 -0
  251. package/templates/ios/ios/{{APP_NAME}}/Shared/Showcase.swift +13 -0
  252. package/templates/ios/ios/{{APP_NAME}}/Shared/SnapshotCache.swift +78 -0
  253. package/templates/ios/ios/{{APP_NAME}}/Shared/UITestEnvironment.swift +70 -0
  254. package/templates/ios/ios/{{APP_NAME}}/{{APP_NAME}}.entitlements +10 -0
  255. package/templates/ios/ios/{{APP_NAME}}Tests/Onboarding/OnboardingGateTests.swift +38 -0
  256. package/templates/ios/ios/{{APP_NAME}}Tests/Settings/SettingsViewModelTests.swift +63 -0
  257. package/templates/ios/ios/{{APP_NAME}}Tests/Support/TestDoubles.swift +35 -0
  258. package/templates/ios/package.merge.json +10 -0
  259. package/templates/ios/scripts/apollo-codegen.ts +31 -0
  260. package/templates/ios/scripts/check-ios-components.ts +72 -0
  261. package/templates/ios/scripts/check-strings.ts +103 -0
  262. package/templates/ios/scripts/install-device.sh +29 -0
  263. package/templates/ios/scripts/testflight.ts +213 -0
@@ -0,0 +1,21 @@
1
+ const LEADING_ARTICLES = /^(the|a|an|le|la|les|l|un|une|des|du|de)[\s-]+/
2
+
3
+ /** Folds a title, a saga name or an author into a key two spellings of the same
4
+ * thing agree on. Diacritics are dropped, punctuation collapses to a separator
5
+ * and a leading article is removed, so "L'Assassin royal" and "Assassin Royal"
6
+ * meet.
7
+ *
8
+ * Shared because two different things are keyed on it and they must agree: the
9
+ * series catalogue's document id, which two readers have to converge on, and the
10
+ * match that tells an import a book is already on the reader's shelves. */
11
+ export const slugify = (value: string): string =>
12
+ value
13
+ .normalize('NFD')
14
+ .replace(/[̀-ͯ]/g, '')
15
+ .toLowerCase()
16
+ .replace(/['’]/g, ' ')
17
+ .replace(/[^a-z0-9]+/g, ' ')
18
+ .trim()
19
+ .replace(LEADING_ARTICLES, '')
20
+ .trim()
21
+ .replace(/\s+/g, '-')
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "./.nitro/types/tsconfig.json",
3
+ "compilerOptions": {
4
+ "strict": true,
5
+ "noImplicitAny": true,
6
+ "noImplicitReturns": true,
7
+ "noFallthroughCasesInSwitch": true,
8
+ "useUnknownInCatchVariables": true,
9
+ "noUnusedLocals": true,
10
+ "resolveJsonModule": true,
11
+ "allowSyntheticDefaultImports": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "types": ["nitropack", "bun-types"],
14
+ "lib": ["ESNext", "DOM"]
15
+ }
16
+ }
@@ -0,0 +1,17 @@
1
+ name: Renovate
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ schedule:
6
+ - cron: '0 20 * * *' # 22h CET / 21h UTC
7
+
8
+ jobs:
9
+ renovate:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v7
13
+ - uses: renovatebot/github-action@v46.3.9
14
+ with:
15
+ token: ${{ secrets.RENOVATE_TOKEN }}
16
+ env:
17
+ RENOVATE_REPOSITORIES: ${{ github.repository }}
@@ -0,0 +1,3 @@
1
+ # Journal des modifications
2
+
3
+ Traduction française de `CHANGELOG.md`, qui fait foi. Les deux fichiers avancent ensemble.
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ English is the source of truth. Every served language has its own file, kept in lockstep:
4
+ `CHANGELOG.fr.md`. Only what a user notices is logged, in their words.
5
+
6
+ Each version heading carries the App Store version and its release date: `## 1.0 (2026.10.01)`.
7
+ Entries are written when an App Store release is cut, never ahead of it.
@@ -0,0 +1,34 @@
1
+ # {{APP_NAME}}
2
+
3
+ <!-- stack:rules -->
4
+ <!-- /stack:rules -->
5
+
6
+ What the app is, in two sentences, for someone who has never opened it.
7
+
8
+ The rules above are the Polyforms stack, version `{{STACK_VERSION}}` (`stack/VERSION`), and
9
+ this file holds only what is specific to this project. The project's facts (ids, accounts,
10
+ devices, certificates, pending work) are in [project-context.md](project-context.md).
11
+
12
+ ## Commands
13
+
14
+ | Purpose | Command |
15
+ |---|---|
16
+ | Typecheck | `bun run typecheck` |
17
+ | Tests, by family | `bun run test:unit`, `bun run test:int`, `bun run test:feat` |
18
+ | Preflight (all of the above plus lint) | `bun run preflight` |
19
+ | Linter | `bun run lint` / `bun run lint:fix` |
20
+ | GraphQL schema | `bun run generate:graphql` |
21
+ | Schema compatibility | `bun run schema:check` |
22
+ | Deprecations | `bun run deprecations` |
23
+ | Dev server | `bun run dev` |
24
+ | Stack in sync | `bun run stack:check` |
25
+
26
+ ## Data model
27
+
28
+ What is private and what is shared, the collections and their keys, the invariants a
29
+ reviewer must know before touching a domain. Written once the first domain exists.
30
+
31
+ ## Traps of this project
32
+
33
+ One line per trap, with the symptom and the way around it. Anything that would hold in
34
+ another project goes into the stack instead of here.
@@ -0,0 +1,12 @@
1
+ # {{APP_NAME}}
2
+
3
+ What the app does, for someone who has never opened it. Why before how, numbered steps, no
4
+ deep architecture and no screenshot that ages.
5
+
6
+ ## Run it
7
+
8
+ 1. `bun install`
9
+ 2. Copy `.env.example` to `.env` and fill it.
10
+ 3. `bun run dev` serves the API on http://localhost:3000 (GraphQL at `POST /graphql`).
11
+
12
+ Built on the Polyforms stack (`stack/VERSION`).
@@ -0,0 +1,47 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.5.15/schema.json",
3
+ "vcs": {
4
+ "enabled": true,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": true
7
+ },
8
+ "files": {
9
+ "ignoreUnknown": true,
10
+ "includes": [
11
+ "**",
12
+ "!shared",
13
+ "!site",
14
+ "!stack",
15
+ "!stack.config.json",
16
+ "!server/system/changelog-content.ts",
17
+ "!ios/**/Assets.xcassets",
18
+ "!ios/**/*.xcstrings"
19
+ ]
20
+ },
21
+ "formatter": {
22
+ "enabled": true,
23
+ "indentStyle": "space",
24
+ "indentWidth": 2,
25
+ "lineWidth": 100
26
+ },
27
+ "linter": {
28
+ "enabled": true,
29
+ "rules": {
30
+ "preset": "recommended",
31
+ "correctness": {
32
+ "noUnusedImports": "error",
33
+ "noUnusedVariables": "error"
34
+ },
35
+ "style": {
36
+ "noNamespace": "error"
37
+ }
38
+ }
39
+ },
40
+ "javascript": {
41
+ "formatter": {
42
+ "quoteStyle": "single",
43
+ "semicolons": "asNeeded",
44
+ "trailingCommas": "all"
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,2 @@
1
+ [install]
2
+ exact = true
@@ -0,0 +1,21 @@
1
+ # 0001. Built on the Polyforms stack
2
+
3
+ Date: {{TODAY}}
4
+
5
+ ## Context
6
+
7
+ Every Polyforms app shares one way of being built: a Bun and Nitro backend on Firebase,
8
+ GraphQL through Pothos, Terraform, one CI workflow per concern, Sentry, scheduled routines,
9
+ and for apps with a phone client, SwiftUI with the stack's conventions.
10
+
11
+ ## Decision
12
+
13
+ This project consumes the `@moifort/stack` package. Its rules are rendered into `stack/`
14
+ and imported by `CLAUDE.md`; its templates seeded the repository; a version bump arrives as
15
+ a Renovate pull request and is applied by the upgrade routine.
16
+
17
+ ## Consequences
18
+
19
+ A rule is never changed here: a correction that holds for every project goes into the
20
+ package and comes back as a version. What is specific to this project is written in
21
+ `CLAUDE.md`, `project-context.md` and the ADRs that follow this one.
@@ -0,0 +1,116 @@
1
+ # Data recovery — Firestore protections and runbook
2
+
3
+ What protects production data, and what to do when it is lost. Read this before touching
4
+ anything: a hurried restore over live writes does more damage than the incident.
5
+
6
+ ## What is in place
7
+
8
+ Both settings live on `google_firestore_database.default` in `infra/firebase.tf`, applied by
9
+ the Deploy workflow (since 2026-10-19, commit 5dc4f24).
10
+
11
+ | Protection | Covers | Retention |
12
+ |---|---|---|
13
+ | Delete protection (`DELETE_PROTECTION_ENABLED`) | Deleting the `(default)` database from the console, `gcloud` or a script | — |
14
+ | Point-in-time recovery (`POINT_IN_TIME_RECOVERY_ENABLED`) | Documents deleted or overwritten by a bug, a migration, a bad command | 7 days, to the minute |
15
+ | Terraform `deletion_policy = ABANDON` (provider default) | `terraform destroy` leaves the database alone | — |
16
+ | GCP project soft delete (platform default) | Deleting the whole project | 30 days |
17
+
18
+ **Not in place, on purpose:** scheduled Firestore backups, an export to a bucket in another
19
+ project, a lien on the project. So there is **no copy of the data outside the database
20
+ itself**: a deleted database or a project gone past 30 days is unrecoverable.
21
+
22
+ Check the live state (read-only, works with the observer account):
23
+
24
+ ```bash
25
+ CLOUDSDK_CONFIG=$HOME/.config/{{APP_SLUG}}-observer/gcloud gcloud firestore databases describe --database='(default)' --project={{APP_SLUG}}-polyforms --format='value(deleteProtectionState,pointInTimeRecoveryEnablement,earliestVersionTime,versionRetentionPeriod)'
26
+ ```
27
+
28
+ `earliestVersionTime` is the oldest instant that can still be recovered.
29
+
30
+ ## Before any recovery
31
+
32
+ 1. **Stop the bleeding first.** If a bug or a migration is still writing, revert and deploy
33
+ the fix (or pause the scheduler job) before restoring, or the restore is overwritten.
34
+ 2. **Pin the instant.** Find the last whole minute *before* the damage, from Cloud Logging,
35
+ the `migration-meta` collection or the deploy time. PITR snapshot times must be a whole
36
+ minute (`…:05:00Z`) and later than `earliestVersionTime`. The clock runs: past seven days
37
+ the version is gone, so do this the day the damage is seen.
38
+ 3. **Scope the damage.** Which collections, which `userId`s, which documents. Collections are
39
+ flat with a `userId` field, so a reader's library is one query.
40
+ 4. Recovery writes production: it needs the user's own `gcloud` login (the observer account is
41
+ read-only) and **the user's go-ahead**. It is never done silently.
42
+
43
+ ## Case 1 — documents deleted or overwritten, less than 7 days ago
44
+
45
+ Never restore the whole database over `(default)`: it would erase every legitimate write made
46
+ since. Recover only what was damaged.
47
+
48
+ **A handful of documents** — read the old version straight from `(default)` with a read-only
49
+ transaction at a past `readTime`, then write it back:
50
+
51
+ ```ts
52
+ import { Timestamp } from 'firebase-admin/firestore'
53
+
54
+ const readTime = Timestamp.fromDate(new Date('2026-10-19T14:05:00Z'))
55
+ const before = await db.runTransaction((tx) => tx.get(db.collection('books').doc(bookId)), {
56
+ readOnly: true,
57
+ readTime,
58
+ })
59
+ if (before.exists) await db.collection('books').doc(bookId).set(before.data()!)
60
+ ```
61
+
62
+ A query works the same way (`tx.get(db.collection('books').where('userId', '==', userId))`).
63
+
64
+ **Many documents or whole collections** — clone the database at that instant into a
65
+ separate database, copy back what was damaged, then delete the clone:
66
+
67
+ ```bash
68
+ gcloud firestore databases clone --project={{APP_SLUG}}-polyforms --source-database='projects/{{APP_SLUG}}-polyforms/databases/(default)' --snapshot-time=2026-10-19T14:05:00Z --destination-database=recovered
69
+ ```
70
+
71
+ Then a one-off script (in `scripts/`, run locally with the user's credentials) opens both
72
+ databases — `getFirestore(app)` and `getFirestore(app, 'recovered')` — and copies back the
73
+ damaged documents, in batches of at most 500 writes. A document deleted by the incident
74
+ exists in `recovered` and not in `(default)`; one created since the incident exists only in
75
+ `(default)` and must be left alone.
76
+
77
+ Delete the clone when done, it bills storage while it exists:
78
+
79
+ ```bash
80
+ gcloud firestore databases delete --database=recovered --project={{APP_SLUG}}-polyforms
81
+ ```
82
+
83
+ If `clone` is refused (flag names change; check `gcloud firestore databases clone --help`),
84
+ the fallback is an export at the same instant into a bucket, then an import into a new
85
+ database: `gcloud firestore export gs://<bucket>/recovery --snapshot-time=… --database='(default)'`,
86
+ create `recovered`, `gcloud firestore import gs://<bucket>/recovery --database=recovered`.
87
+
88
+ ## Case 2 — the `(default)` database deleted
89
+
90
+ Delete protection refuses this; someone has to turn it off first
91
+ (`gcloud firestore databases update --database='(default)' --no-delete-protection`, or the
92
+ console). If it happened anyway, the PITR history went with the database and **there is no
93
+ backup to restore from**. What is left is to recreate the database by applying Terraform
94
+ (the Deploy workflow) and to tell readers. Shared series catalogues rebuild themselves from
95
+ new scans; private books do not.
96
+
97
+ ## Case 3 — the whole GCP project deleted
98
+
99
+ A deleted project stays in "pending deletion" for 30 days: everything is shut down, nothing is
100
+ erased. Restore it, with the user's own login (owner):
101
+
102
+ ```bash
103
+ gcloud projects undelete {{APP_SLUG}}-polyforms
104
+ ```
105
+
106
+ Then check billing is still linked (the prepaid Gemini key and Cloud Run need it), that Cloud
107
+ Run serves again, and that Firestore answers. APIs and services come back with the project;
108
+ re-run the Deploy workflow if anything is missing. Past 30 days the project, its data and its
109
+ ID are gone for good.
110
+
111
+ ## If more protection is ever wanted
112
+
113
+ From cheapest to strongest: a lien on the project (free, blocks project deletion until
114
+ removed); a `google_firestore_backup_schedule` (daily kept 7 days, weekly kept 14 weeks,
115
+ billed as backup storage, restore into a new database); a scheduled export to a bucket in
116
+ another project with a retention lock — the only copy that survives losing the project.
@@ -0,0 +1,4 @@
1
+ # Ideas
2
+
3
+ One line per idea, dated, newest first. Nothing here is decided; an idea that becomes a
4
+ feature leaves this list for a design or an ADR.
@@ -0,0 +1,43 @@
1
+ # Project context
2
+
3
+ The facts of this project. Nothing here is a rule (rules live in the stack) and nothing
4
+ here should be copied into another project.
5
+
6
+ ## Environments
7
+
8
+ | Environment | GCP project | Notes |
9
+ |---|---|---|
10
+ | Production | `{{APP_SLUG}}-polyforms` | |
11
+ | Staging | `{{APP_SLUG}}-staging-polyforms` | Deployed from pull requests labelled `staging` |
12
+
13
+ ## Accounts and consoles
14
+
15
+ Which account opens which console, and the access trick if there is one.
16
+
17
+ ## Analytics and monitoring
18
+
19
+ Sentry projects `{{APP_SLUG}}-node` and `{{APP_SLUG}}-ios` (org `polyforms`). GA4 property, dashboards.
20
+
21
+ ## Test devices
22
+
23
+ Name, identifier, signing team, trust state.
24
+
25
+ ## Certificates and keys
26
+
27
+ | Item | Role | Created | Expires |
28
+ |---|---|---|---|
29
+ | | Signing | | |
30
+ | | App Store Connect API | | |
31
+
32
+ ## Where each secret lives
33
+
34
+ Every token and the places it is replicated, with the example file beside each ignored one.
35
+
36
+ ## Decisions specific to this project
37
+
38
+ Structuring choices and their reason, when neither the code nor the history says it. An
39
+ architecture choice gets an ADR in `docs/adr/`.
40
+
41
+ ## Pending work
42
+
43
+ What is started, what remains to be done by hand, what waits on someone else.
@@ -0,0 +1,37 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": ["config:recommended"],
4
+ "pinDigests": false,
5
+ "gitAuthor": "Renovate Bot <{{GITHUB_OWNER}}@users.noreply.github.com>",
6
+ "labels": ["dependencies"],
7
+ "packageRules": [
8
+ {
9
+ "description": "Automerge non-major updates in a single grouped PR",
10
+ "matchUpdateTypes": ["minor", "patch", "digest"],
11
+ "groupName": "non-major dependencies",
12
+ "commitMessageTopic": "dependencies",
13
+ "automerge": true,
14
+ "automergeType": "pr",
15
+ "automergeStrategy": "squash"
16
+ },
17
+ {
18
+ "description": "Major updates: one pull request each, never automerged; the upgrade routine reads the changelog, applies the code changes, deploys to staging and merges",
19
+ "matchUpdateTypes": ["major"],
20
+ "labels": ["dependencies", "major"],
21
+ "automerge": false
22
+ },
23
+ {
24
+ "description": "The stack package: never automerged; the upgrade routine applies its migrations and merges",
25
+ "matchPackageNames": ["@moifort/stack"],
26
+ "labels": ["dependencies", "stack"],
27
+ "automerge": false
28
+ }
29
+ ],
30
+ "lockFileMaintenance": {
31
+ "enabled": true,
32
+ "schedule": ["before 5am on monday"],
33
+ "automerge": true,
34
+ "automergeType": "pr",
35
+ "automergeStrategy": "squash"
36
+ }
37
+ }
@@ -0,0 +1,168 @@
1
+ name: Build
2
+
3
+ # Runs only for what the app is built from: its sources, the committed schema the
4
+ # Apollo code is regenerated from (so a backend change that breaks an iOS operation
5
+ # still lands here), the two checks the job ends with, and this file. A server-only
6
+ # push no longer pays twenty minutes of macOS runner, nor cancels a running build.
7
+ # No branch protection requires this check, so a run that never starts blocks nothing.
8
+ on:
9
+ push:
10
+ branches: [main]
11
+ paths:
12
+ - 'ios/**'
13
+ - 'shared/schema.graphql'
14
+ - 'scripts/check-strings.ts'
15
+ - 'scripts/check-ios-components.ts'
16
+ - '.github/workflows/build-ios.yml'
17
+ pull_request:
18
+ branches: [main]
19
+ paths:
20
+ - 'ios/**'
21
+ - 'shared/schema.graphql'
22
+ - 'scripts/check-strings.ts'
23
+ - 'scripts/check-ios-components.ts'
24
+ - '.github/workflows/build-ios.yml'
25
+ workflow_dispatch:
26
+
27
+ # A newer push to the same branch, or to the same pull request, makes this run
28
+ # pointless: it is cancelled rather than left to use a runner for nothing. Keyed on
29
+ # the workflow file and the ref, since the three test workflows share one name.
30
+ concurrency:
31
+ group: ${{ github.workflow_ref }}
32
+ cancel-in-progress: true
33
+
34
+ permissions:
35
+ contents: read
36
+
37
+ jobs:
38
+ # Guards the one thing the other three workflows cannot see: whether the app
39
+ # still compiles. The generated Apollo code is regenerated here from the
40
+ # committed schema, so a backend change that the iOS operations no longer match
41
+ # fails here rather than at release time. Builds without signing or uploading.
42
+ # macos-26 is a standard runner, so this costs nothing on a public repository.
43
+ ios:
44
+ name: 📱 iOS
45
+ runs-on: macos-26
46
+ # The simulator's first boot on a fresh runner takes ten minutes and more,
47
+ # on top of a cold build: 30 cut a run short on 2026-10-16.
48
+ timeout-minutes: 45
49
+ steps:
50
+ - uses: actions/checkout@v7
51
+
52
+ - name: Select the latest final Xcode
53
+ uses: maxim-lobanov/setup-xcode@v1
54
+ with:
55
+ xcode-version: latest-stable
56
+
57
+ # Booted first, so its ten-minute first start runs alongside the Apollo
58
+ # generation and the build rather than after them, where it sat as eleven
59
+ # silent minutes before the tests on 2026-10-16.
60
+ #
61
+ # The iPhone 17 precisely: the scheme's test pre-action boots "iPhone 17" to
62
+ # add the UI tests' photo. On any other device that pre-action cold-booted a
63
+ # second simulator inside the test step, six to fifteen silent minutes before
64
+ # the first test on 2026-10-18–20. simctl resolves the name to the device
65
+ # already booted, so here it finds this one ready and does nothing.
66
+ - name: Boot the simulator the tests run on
67
+ run: |
68
+ set -euo pipefail
69
+ devices=$(xcrun simctl list devices available)
70
+ line=$(grep -m1 -E '^ +iPhone 17 \(' <<< "$devices" \
71
+ || grep -m1 -E '^ +iPhone' <<< "$devices")
72
+ udid=$(grep -oE '[0-9A-F]{8}(-[0-9A-F]{4}){3}-[0-9A-F]{12}' <<< "$line")
73
+ echo "Testing on $line"
74
+ echo "SIMULATOR_UDID=$udid" >> "$GITHUB_ENV"
75
+ xcrun simctl boot "$udid"
76
+
77
+ - name: Generate the Xcode project
78
+ run: |
79
+ brew install xcodegen
80
+ (cd ios && xcodegen generate)
81
+
82
+ - name: Materialize gitignored build files
83
+ run: |
84
+ set -euo pipefail
85
+ printf '%s' "${{ secrets.IOS_GOOGLE_SERVICE_INFO_PLIST }}" \
86
+ | openssl base64 -d -A > ios/{{APP_NAME}}/GoogleService-Info.plist
87
+ cp ios/{{APP_NAME}}/Shared/Secrets.swift.example ios/{{APP_NAME}}/Shared/Secrets.swift
88
+
89
+ # Fetching the packages took four of the Apollo step's minutes on 2026-10-20.
90
+ # Package.resolved is not committed, so the cache cannot be keyed on it before
91
+ # resolving: the latest cache is restored, resolution then fetches only what
92
+ # moved, and a new cache is saved only when the resolved versions changed.
93
+ - name: Restore the Swift packages
94
+ id: spm-restore
95
+ uses: actions/cache/restore@v6
96
+ with:
97
+ path: ${{ runner.temp }}/SourcePackages
98
+ key: spm-${{ runner.os }}-${{ github.sha }}
99
+ restore-keys: spm-${{ runner.os }}-
100
+
101
+ - name: Generate Apollo GraphQL code (gitignored, regenerated per build)
102
+ run: |
103
+ set -euo pipefail
104
+ # Resolve SPM so the apollo-ios checkout (and its bundled CLI) exists.
105
+ xcodebuild -resolvePackageDependencies \
106
+ -project ios/{{APP_NAME}}.xcodeproj -scheme {{APP_NAME}} \
107
+ -derivedDataPath "$RUNNER_TEMP/DerivedData" \
108
+ -clonedSourcePackagesDirPath "$RUNNER_TEMP/SourcePackages"
109
+ resolved=ios/{{APP_NAME}}.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
110
+ if [ -f "$resolved" ]; then
111
+ echo "SPM_CACHE_KEY=spm-${RUNNER_OS}-$(shasum -a 256 "$resolved" | cut -c1-16)" \
112
+ >> "$GITHUB_ENV"
113
+ fi
114
+ # Apollo 2.x ships the CLI as a tarball inside the checkout — extract it.
115
+ CLI_DIR="$RUNNER_TEMP/SourcePackages/checkouts/apollo-ios/CLI"
116
+ tar -xzf "$CLI_DIR/apollo-ios-cli.tar.gz" -C "$CLI_DIR"
117
+ chmod +x "$CLI_DIR/apollo-ios-cli"
118
+ (cd ios && "$CLI_DIR/apollo-ios-cli" generate)
119
+
120
+ - name: Save the Swift packages
121
+ if: env.SPM_CACHE_KEY != '' && steps.spm-restore.outputs.cache-matched-key != env.SPM_CACHE_KEY
122
+ uses: actions/cache/save@v6
123
+ with:
124
+ path: ${{ runner.temp }}/SourcePackages
125
+ key: ${{ env.SPM_CACHE_KEY }}
126
+
127
+ - name: Build the app and its tests
128
+ # Compiled once, for the simulator the tests then run on: no signing
129
+ # identity needed (release-ios.yml owns those).
130
+ run: |
131
+ set -euo pipefail
132
+ xcodebuild -project ios/{{APP_NAME}}.xcodeproj -scheme {{APP_NAME}} \
133
+ -destination "id=$SIMULATOR_UDID" \
134
+ -derivedDataPath "$RUNNER_TEMP/DerivedData" \
135
+ -clonedSourcePackagesDirPath "$RUNNER_TEMP/SourcePackages" \
136
+ CODE_SIGNING_ALLOWED=NO \
137
+ build-for-testing
138
+
139
+ - name: Unit tests
140
+ # The ViewModels driven over inline clients ({{APP_NAME}}Tests): no network, no
141
+ # Firebase, only a simulator to host the bundle. Runs what the step above
142
+ # built, without compiling anything again.
143
+ run: |
144
+ set -euo pipefail
145
+ # Says how long the boot still had to go, so a slow one shows here.
146
+ start=$SECONDS
147
+ xcrun simctl bootstatus "$SIMULATOR_UDID" -b
148
+ echo "Simulator ready after a further $((SECONDS - start)) s"
149
+ xcodebuild -project ios/{{APP_NAME}}.xcodeproj -scheme {{APP_NAME}} \
150
+ -destination "id=$SIMULATOR_UDID" \
151
+ -derivedDataPath "$RUNNER_TEMP/DerivedData" \
152
+ -clonedSourcePackagesDirPath "$RUNNER_TEMP/SourcePackages" \
153
+ -only-testing:{{APP_NAME}}Tests \
154
+ CODE_SIGNING_ALLOWED=NO \
155
+ test-without-building
156
+
157
+ - uses: oven-sh/setup-bun@v2
158
+
159
+ - name: Every string has its French and its English
160
+ # Reads the strings the build just extracted: one added to the code and
161
+ # left out of the catalogue fails here, as does one with no English.
162
+ run: bun scripts/check-strings.ts "$RUNNER_TEMP/DerivedData"
163
+
164
+ - name: Components stay pure and previewed
165
+ # Stack rule 10-ios.md: nothing under components/ or Shared/Components
166
+ # calls the server, reads a singleton or holds a ViewModel,
167
+ # and every view there has a #Preview.
168
+ run: bun scripts/check-ios-components.ts