@clubedge/create-clubedge-app 0.44.0 → 0.45.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 +13 -0
- package/README.md +12 -10
- package/dist/core/merge.js +26 -12
- package/dist/core/package-json-merge.js +106 -0
- package/package.json +3 -3
- package/template/.github/workflows/ci.yml +1 -1
- package/template/.railway/railway.ts +9 -0
- package/template/README.md +24 -15
- package/template/SETUP.md +49 -23
- package/template/apps/api/.env.example +22 -5
- package/template/apps/api/package.json +1 -1
- package/template/apps/api/src/app.test.ts +24 -1
- package/template/apps/api/src/app.ts +45 -3
- package/template/apps/api/src/config/site.json +3 -1
- package/template/apps/api/src/routes/app-version.test.ts +68 -7
- package/template/apps/api/src/routes/app-version.ts +10 -3
- package/template/apps/api/src/server/app-version.test.ts +109 -7
- package/template/apps/api/src/server/app-version.ts +94 -44
- package/template/apps/api-fastapi/.env.example +26 -9
- package/template/apps/api-fastapi/package.json +1 -1
- package/template/apps/api-fastapi/pyproject.toml +1 -1
- package/template/apps/api-fastapi/src/clubedge_api/app.py +41 -3
- package/template/apps/api-fastapi/src/clubedge_api/app_version.py +160 -28
- package/template/apps/api-fastapi/src/clubedge_api/contract.py +4 -0
- package/template/apps/api-fastapi/src/clubedge_api/routes/app_version.py +17 -8
- package/template/apps/api-fastapi/src/config/site.json +3 -1
- package/template/apps/api-fastapi/tests/test_app_version.py +247 -20
- package/template/apps/api-fastapi/uv.lock +1 -1
- package/template/apps/capacitor/assets/splash-dark.svg +1 -0
- package/template/apps/capacitor/assets/splash-icon.png +0 -0
- package/template/apps/capacitor/assets/splash.svg +1 -0
- package/template/apps/capacitor/package.json +1 -1
- package/template/apps/capacitor/scripts/icons.mjs +113 -14
- package/template/apps/expo/app.config.ts +4 -1
- package/template/apps/expo/clubedge.stack.json +6 -1
- package/template/apps/expo/package.json +7 -2
- package/template/apps/expo/src/app/(app)/_layout.tsx +4 -23
- package/template/apps/expo/src/app/(app)/index.tsx +164 -44
- package/template/apps/expo/src/app/(app)/settings/_layout.tsx +12 -0
- package/template/apps/expo/src/app/(app)/settings/delete-account.tsx +76 -0
- package/template/apps/expo/src/app/(app)/settings/edit-profile.tsx +44 -0
- package/template/apps/expo/src/app/(app)/settings/index.tsx +90 -0
- package/template/apps/expo/src/app/(app)/settings/password.tsx +49 -0
- package/template/apps/expo/src/app/_layout.tsx +12 -1
- package/template/apps/expo/src/app/forgot-password.tsx +47 -39
- package/template/apps/expo/src/app/reset-password.tsx +6 -8
- package/template/apps/expo/src/app/sign-in.tsx +29 -12
- package/template/apps/expo/src/app/sign-up.tsx +42 -14
- package/template/apps/expo/src/app/welcome.tsx +99 -0
- package/template/apps/expo/src/components/sign-out-sheet.tsx +138 -0
- package/template/apps/expo/src/components/tab-bar.tsx +143 -0
- package/template/apps/expo/src/components/ui.tsx +545 -81
- package/template/apps/expo/src/components/update-gate.tsx +327 -37
- package/template/apps/expo/src/config/site.json +3 -1
- package/template/apps/expo/src/lib/account.ts +62 -0
- package/template/apps/expo/src/lib/app-update.ts +36 -0
- package/template/apps/expo/src/lib/auth-client.ts +10 -2
- package/template/apps/expo/src/lib/theme.ts +152 -15
- package/template/apps/expo/src/types/images.d.ts +13 -0
- package/template/apps/expo/src/variants/app-layout.auth.tsx +17 -0
- package/template/apps/expo/src/variants/auth.better-auth.ts +12 -2
- package/template/apps/expo/src/variants/auth.supabase.ts +8 -5
- package/template/apps/expo/src/variants/layout.auth.tsx +24 -13
- package/template/apps/start/package.json +1 -1
- package/template/apps/start/src/config/site.json +3 -1
- package/template/apps/vite/.env.example +4 -4
- package/template/apps/vite/package.json +1 -1
- package/template/apps/vite/server.mjs +2 -1
- package/template/apps/vite/src/config/site.json +3 -1
- package/template/apps/vite/src/main.tsx +4 -3
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/src/config/site.json +3 -1
- package/template/clubedge.template.json +1 -1
- package/template/package.json +3 -2
- package/template/packages/auth/package.json +1 -1
- package/template/packages/auth-appwrite/package.json +1 -1
- package/template/packages/auth-better-auth/package.json +1 -1
- package/template/packages/auth-firebase/package.json +1 -1
- package/template/packages/auth-supabase/package.json +1 -1
- package/template/packages/cache/package.json +1 -1
- package/template/packages/cache-redis/package.json +1 -1
- package/template/packages/contract/openapi.yaml +85 -3
- package/template/packages/contract/package.json +1 -1
- package/template/packages/contract/src/app-update.test.ts +321 -0
- package/template/packages/contract/src/app-update.ts +237 -0
- package/template/packages/contract/src/app-version.test.ts +76 -0
- package/template/packages/contract/src/app-version.ts +71 -0
- package/template/packages/contract/src/conformance.ts +42 -1
- package/template/packages/contract/src/index.ts +2 -0
- package/template/packages/contract/src/schema.ts +44 -4
- package/template/packages/core/package.json +1 -1
- package/template/packages/db/package.json +1 -1
- package/template/packages/email/package.json +1 -1
- package/template/packages/email-resend/package.json +1 -1
- package/template/packages/email-smtp/package.json +1 -1
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/jobs-bullmq/package.json +1 -1
- package/template/packages/jobs-inngest/package.json +1 -1
- package/template/packages/jobs-pg-boss/package.json +1 -1
- package/template/packages/local-db/package.json +1 -1
- package/template/packages/mobile/package.json +1 -1
- package/template/packages/mobile/src/demo/api.ts +7 -1
- package/template/packages/mobile/src/demo/demo.test.ts +2 -0
- package/template/packages/mobile/src/update-notice.test.ts +26 -2
- package/template/packages/mobile/src/update-notice.tsx +55 -41
- package/template/packages/observability/package.json +1 -1
- package/template/packages/observability-otel/package.json +1 -1
- package/template/packages/observability-sentry/package.json +1 -1
- package/template/packages/payments/package.json +1 -1
- package/template/packages/payments-stripe/package.json +1 -1
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage-s3/package.json +1 -1
- package/template/packages/storage-supabase/package.json +1 -1
- package/template/packages/ui/package.json +1 -1
- package/template/pnpm-lock.yaml +146 -5
- package/template/supabase/config.toml +10 -0
- package/template.lock.json +21 -7
- package/template/apps/expo/src/app/(app)/settings.tsx +0 -168
- package/template/apps/expo/src/lib/update.test.ts +0 -34
- package/template/apps/expo/src/lib/update.ts +0 -40
- package/template/packages/mobile/src/version.test.ts +0 -65
- package/template/packages/mobile/src/version.ts +0 -37
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `create-clubedge-app` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.45.0
|
|
6
|
+
|
|
7
|
+
- **Starter v0.29.0** by default (#159). Its template schema is still 6. The [Starter's release notes](https://github.com/Clubedge/clubedge-starter/releases/tag/v0.29.0) cover:
|
|
8
|
+
- an update policy for apps installed on phones: a minimum announced for a date, values per platform, and APIs that answer apps below the minimum `426 update_required`;
|
|
9
|
+
- a new design for the native app (`--mobile expo`);
|
|
10
|
+
- splash screens that are never cropped or stretched.
|
|
11
|
+
- **`package.json` merges by key** (#158) when `add`, `remove`, or `upgrade` merges the Starter's changes into your project:
|
|
12
|
+
- A key only one side changed takes that side's value, and objects merge key by key, so your dependencies and the Starter's no longer collide line by line.
|
|
13
|
+
- A key both sides changed differently is a conflict, with the usual markers around that key's lines only.
|
|
14
|
+
- Your key order and indentation are kept. A file that doesn't parse, such as one with markers left from an earlier merge, merges by line as before.
|
|
15
|
+
- **The Starter pin** keeps the release a change came with in notes such as "since CLI 0.44.0, with Starter v0.28.0", which moving the pin to v0.29.0 had changed.
|
|
16
|
+
- CI uses astral-sh/setup-uv 10.3.2 (#154).
|
|
17
|
+
|
|
5
18
|
## 0.44.0
|
|
6
19
|
|
|
7
20
|
- **The native app with Expo**: default to Starter `v0.28.0` (#151). `--mobile expo` adds `apps/mobile`, an Android and iOS app with its own native screens, which calls the project's API (#147):
|
package/README.md
CHANGED
|
@@ -241,7 +241,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git --framework tanstac
|
|
|
241
241
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref main
|
|
242
242
|
|
|
243
243
|
# Reproduce a specific Starter release or commit
|
|
244
|
-
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.
|
|
244
|
+
pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.29.0
|
|
245
245
|
pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
|
|
246
246
|
|
|
247
247
|
# Test local Starter changes before releasing them
|
|
@@ -260,13 +260,14 @@ pnpm clubedge add email resend payments stripe # several modules as
|
|
|
260
260
|
pnpm clubedge remove storage payments
|
|
261
261
|
pnpm dlx @clubedge/create-clubedge-app add infra local --dry-run
|
|
262
262
|
pnpm dlx @clubedge/create-clubedge-app@latest upgrade # to the newest CLI's Starter release
|
|
263
|
-
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.
|
|
263
|
+
pnpm dlx @clubedge/create-clubedge-app upgrade --ref v0.29.0 # or a specific tag, branch, or commit
|
|
264
264
|
```
|
|
265
265
|
|
|
266
266
|
The CLI renders the project twice, from the Starter commit and modules recorded in your `package.json` and from the new state, and merges the difference into your project. The new state has other modules for `add` and `remove`, and the newer Starter with the same modules for `upgrade`. Modules a newer Starter introduces start as `none` where possible, so an upgrade never adds a service on its own.
|
|
267
267
|
|
|
268
268
|
- Files you never changed are updated, added, or deleted.
|
|
269
269
|
- Files you changed keep your edits. Where the module change touches the same lines, the file gets conflict markers (`<<<<<<< yours` ... `>>>>>>> clubedge`) for you to resolve. The Starter's whole new version of each such file is saved in `.clubedge/incoming/` to compare with yours (`git diff --no-index <file> .clubedge/incoming/<file>`); Git ignores that folder, so delete it when you are done.
|
|
270
|
+
- `package.json` files merge by key, not by line: a script, dependency, or override you added next to the Starter's keeps both, and markers wrap only a key you both changed to different values, such as the same dependency's range. The file keeps your key order, indentation, and line endings.
|
|
270
271
|
- Your own files are never touched.
|
|
271
272
|
- Each app's local env file (`apps/web/.env.local`, and `apps/api/.env.local`, `apps/shell/.env.local`, or `apps/mobile/.env.local` when the project has them) keeps your values. Values still at their generated defaults follow the new selection, and new variables are added.
|
|
272
273
|
- `pnpm-lock.yaml` is replaced if you never changed your dependencies; otherwise `pnpm install` reconciles it.
|
|
@@ -399,27 +400,27 @@ Projects created with `--infra neon` also have `.github/workflows/preview.yml`,
|
|
|
399
400
|
Each CLI release is tested against one specific Starter release:
|
|
400
401
|
|
|
401
402
|
```text
|
|
402
|
-
create-clubedge-app v0.
|
|
403
|
+
create-clubedge-app v0.45.0
|
|
403
404
|
|
|
|
404
405
|
v
|
|
405
|
-
Clubedge Starter v0.
|
|
406
|
-
commit
|
|
406
|
+
Clubedge Starter v0.29.0
|
|
407
|
+
commit 29350332db4bc44acc1e7b989c04825ff4c0f7b7
|
|
407
408
|
```
|
|
408
409
|
|
|
409
410
|
The current relationship is:
|
|
410
411
|
|
|
411
412
|
| CLI release | Default Starter release | Meaning |
|
|
412
413
|
| --- | --- | --- |
|
|
413
|
-
| `create-clubedge-app@0.
|
|
414
|
+
| `create-clubedge-app@0.45.0` | `clubedge-starter@v0.29.0` | The CLI's required, tested default |
|
|
414
415
|
|
|
415
416
|
This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
|
|
416
417
|
|
|
417
|
-
The `v0.
|
|
418
|
-
`
|
|
418
|
+
The `v0.29.0` tag is expected to resolve to commit
|
|
419
|
+
`29350332db4bc44acc1e7b989c04825ff4c0f7b7`. The CLI verifies this before
|
|
419
420
|
scaffolding with the default reference and fails rather than silently using a
|
|
420
421
|
moved tag.
|
|
421
422
|
|
|
422
|
-
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.
|
|
423
|
+
When the Starter changes, its maintainers first publish a new versioned release, such as `v0.29.0`. The CLI is then updated to use that release, all generated-project checks must pass, and a new CLI version is published. This keeps Starter and CLI releases independently versioned while preserving a tested compatibility relationship.
|
|
423
424
|
|
|
424
425
|
Every generated project records its CLI version, Starter repository, Starter ref, Starter commit, framework, and modules in `package.json` and `README.md`.
|
|
425
426
|
|
|
@@ -474,7 +475,7 @@ CI packs the CLI with `npm pack`, scaffolds clean projects from the packed tarba
|
|
|
474
475
|
1. Make and validate changes in `clubedge-starter`.
|
|
475
476
|
2. Run lint, typecheck, unit tests, browser tests, and production build.
|
|
476
477
|
3. Update the Starter package versions.
|
|
477
|
-
4. Commit `main`, create a release tag such as `v0.
|
|
478
|
+
4. Commit `main`, create a release tag such as `v0.29.0`, and publish the Starter release.
|
|
478
479
|
|
|
479
480
|
### CLI maintainers
|
|
480
481
|
|
|
@@ -529,6 +530,7 @@ Each phase ends with a Starter release and a CLI release pinned to it. The full
|
|
|
529
530
|
| 0.42.0 | v0.27.0 | Starter v0.27.0: mobile apps with Capacitor (`--shell capacitor`, template schema 5); client addresses per platform; Docker Compose behind Caddy, with images on GHCR |
|
|
530
531
|
| 0.43.0 | v0.27.1 | Upgrades keep each app's own version; add, remove, and upgrade that change nothing skip the Starter download |
|
|
531
532
|
| 0.44.0 | v0.28.0 | Starter v0.28.0: the native app with Expo (`--mobile expo`, template schema 6); projects without a website (`--framework none`), with template rules; audit exceptions per stack |
|
|
533
|
+
| 0.45.0 | v0.29.0 | Starter v0.29.0: an update policy for installed apps, a new design for the native app, and clean splash screens; `package.json` merged by key |
|
|
532
534
|
|
|
533
535
|
## Community
|
|
534
536
|
|
package/dist/core/merge.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Three-way merge between what the CLI generated (base), what it would generate now (target),
|
|
2
2
|
// and the project as it is (current). Changes from base to target are applied to the project
|
|
3
3
|
// without losing the project's own edits. Pure: callers read and write the files.
|
|
4
|
-
import { diff3Merge } from "node-diff3";
|
|
4
|
+
import { diff3Merge, diffComm } from "node-diff3";
|
|
5
|
+
import { mergePackageJson } from "./package-json-merge.js";
|
|
5
6
|
export const CONFLICT_LABELS = { yours: "yours", theirs: "clubedge" };
|
|
6
7
|
/**
|
|
7
8
|
* Where the Starter's versions of files left to resolve are saved, next to the project's own,
|
|
@@ -35,28 +36,41 @@ export function sameContent(a, b) {
|
|
|
35
36
|
return a.equals(b);
|
|
36
37
|
return normalize(a) === normalize(b);
|
|
37
38
|
}
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
if (isBinary(current) || isBinary(base) || isBinary(target))
|
|
41
|
-
return null;
|
|
42
|
-
const split = (content) => normalize(content).split("\n");
|
|
43
|
-
const regions = diff3Merge(split(current), split(base), split(target), { excludeFalseConflicts: true });
|
|
39
|
+
/** The merged text, with conflict markers around each region where the two sides differ. */
|
|
40
|
+
function withMarkers(regions) {
|
|
44
41
|
const lines = [];
|
|
45
42
|
let conflict = false;
|
|
46
43
|
for (const region of regions) {
|
|
47
|
-
if (region
|
|
44
|
+
if ("ok" in region) {
|
|
48
45
|
lines.push(...region.ok);
|
|
49
46
|
continue;
|
|
50
47
|
}
|
|
51
|
-
if (!region.conflict)
|
|
52
|
-
continue;
|
|
53
48
|
conflict = true;
|
|
54
|
-
lines.push(`<<<<<<< ${CONFLICT_LABELS.yours}`, ...region.
|
|
49
|
+
lines.push(`<<<<<<< ${CONFLICT_LABELS.yours}`, ...region.yours, "=======", ...region.theirs, `>>>>>>> ${CONFLICT_LABELS.theirs}`);
|
|
55
50
|
}
|
|
56
51
|
return { text: lines.join("\n"), conflict };
|
|
57
52
|
}
|
|
53
|
+
/** Merges base -> target into current. Resolves null for binary files, which cannot be merged. */
|
|
54
|
+
export function mergeText(current, base, target) {
|
|
55
|
+
if (isBinary(current) || isBinary(base) || isBinary(target))
|
|
56
|
+
return null;
|
|
57
|
+
const split = (content) => normalize(content).split("\n");
|
|
58
|
+
const regions = diff3Merge(split(current), split(base), split(target), { excludeFalseConflicts: true });
|
|
59
|
+
return withMarkers(regions.flatMap((region) => region.ok ? [{ ok: region.ok }] : region.conflict ? [{ yours: region.conflict.a, theirs: region.conflict.b }] : []));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Merges a package.json by key (see package-json-merge.ts), so edits to different keys never
|
|
63
|
+
* conflict, and markers wrap only the lines of a key both sides changed differently. Resolves
|
|
64
|
+
* null when a side does not parse, for mergeText to merge it by line.
|
|
65
|
+
*/
|
|
66
|
+
export function mergePackageJsonText(current, base, target) {
|
|
67
|
+
const merged = mergePackageJson(normalize(current), normalize(base), normalize(target));
|
|
68
|
+
if (!merged)
|
|
69
|
+
return null;
|
|
70
|
+
return withMarkers(diffComm(merged.yours.split("\n"), merged.theirs.split("\n")).map((region) => region.common !== undefined ? { ok: region.common } : { yours: region.buffer1, theirs: region.buffer2 }));
|
|
71
|
+
}
|
|
58
72
|
function merged(path, current, base, target, binaryReason) {
|
|
59
|
-
const result = mergeText(current, base, target);
|
|
73
|
+
const result = (/(^|\/)package\.json$/.test(path) ? mergePackageJsonText(current, base, target) : null) ?? mergeText(current, base, target);
|
|
60
74
|
if (!result)
|
|
61
75
|
return { path, kind: "keep", reason: binaryReason, incoming: target };
|
|
62
76
|
const content = withLineEndings(result.text, current);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// package.json files merge by key instead of by line (#142), so edits to different keys never
|
|
2
|
+
// conflict: a script the project adds after the Starter's, or an override next to one the
|
|
3
|
+
// Starter adds. A key both sides changed to different values is a conflict. The merged file is
|
|
4
|
+
// written twice, with your value for each such key and with the Starter's, and merge.ts puts
|
|
5
|
+
// conflict markers around the lines where the two differ, which are that key's.
|
|
6
|
+
/** A key both sides changed to different values; undefined is a side that removed it. */
|
|
7
|
+
class Conflict {
|
|
8
|
+
yours;
|
|
9
|
+
theirs;
|
|
10
|
+
constructor(yours, theirs) {
|
|
11
|
+
this.yours = yours;
|
|
12
|
+
this.theirs = theirs;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Conflict);
|
|
16
|
+
const own = (object, key) => (Object.hasOwn(object, key) ? object[key] : undefined);
|
|
17
|
+
/** Sets a key as data, so a key named __proto__ cannot change the object's prototype. */
|
|
18
|
+
function set(object, key, value) {
|
|
19
|
+
Object.defineProperty(object, key, { value, enumerable: true, writable: true, configurable: true });
|
|
20
|
+
}
|
|
21
|
+
/** Equal JSON values; objects regardless of their key order. */
|
|
22
|
+
function same(a, b) {
|
|
23
|
+
if (a === b)
|
|
24
|
+
return true;
|
|
25
|
+
if (Array.isArray(a) && Array.isArray(b))
|
|
26
|
+
return a.length === b.length && a.every((item, i) => same(item, b[i]));
|
|
27
|
+
if (isObject(a) && isObject(b)) {
|
|
28
|
+
const keys = Object.keys(a);
|
|
29
|
+
return keys.length === Object.keys(b).length && keys.every((key) => Object.hasOwn(b, key) && same(a[key], b[key]));
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* One key's value: whoever changed it, or a conflict when both did, differently. Objects both
|
|
35
|
+
* changed merge key by key; arrays and scalars are single values. Undefined is a removed key.
|
|
36
|
+
*/
|
|
37
|
+
function merge(base, yours, theirs) {
|
|
38
|
+
if (same(yours, theirs) || same(base, theirs))
|
|
39
|
+
return yours;
|
|
40
|
+
if (same(base, yours))
|
|
41
|
+
return theirs;
|
|
42
|
+
if (isObject(yours) && isObject(theirs))
|
|
43
|
+
return mergeObjects(isObject(base) ? base : {}, yours, theirs);
|
|
44
|
+
return new Conflict(yours, theirs);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Your keys in your order; a key only the Starter has goes after the key it follows in the
|
|
48
|
+
* Starter's file, or first. At the root, `clubedge` (the provenance) is always the Starter's.
|
|
49
|
+
*/
|
|
50
|
+
function mergeObjects(base, yours, theirs, root = false) {
|
|
51
|
+
const order = Object.keys(yours);
|
|
52
|
+
let previous;
|
|
53
|
+
for (const key of Object.keys(theirs)) {
|
|
54
|
+
if (!order.includes(key))
|
|
55
|
+
order.splice(previous === undefined ? 0 : order.indexOf(previous) + 1, 0, key);
|
|
56
|
+
previous = key;
|
|
57
|
+
}
|
|
58
|
+
const result = {};
|
|
59
|
+
for (const key of order) {
|
|
60
|
+
const value = root && key === "clubedge" ? own(theirs, key) : merge(own(base, key), own(yours, key), own(theirs, key));
|
|
61
|
+
if (value !== undefined)
|
|
62
|
+
set(result, key, value);
|
|
63
|
+
}
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
/** The merged value with one side's value for each conflict. */
|
|
67
|
+
function resolve(value, side) {
|
|
68
|
+
if (value instanceof Conflict)
|
|
69
|
+
return value[side];
|
|
70
|
+
if (!isObject(value))
|
|
71
|
+
return value;
|
|
72
|
+
const result = {};
|
|
73
|
+
for (const [key, item] of Object.entries(value)) {
|
|
74
|
+
const resolved = resolve(item, side);
|
|
75
|
+
if (resolved !== undefined)
|
|
76
|
+
set(result, key, resolved);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The file's indentation, as npm reads it: the whitespace before the first key, after the
|
|
82
|
+
* opening brace's line. A file on one line has none, and stays on one line.
|
|
83
|
+
*/
|
|
84
|
+
function indentation(text) {
|
|
85
|
+
return /^\s*\{\n([ \t]*)"/.exec(text)?.[1] ?? "";
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Merges base -> target into current, all LF text. Resolves the result written with your value
|
|
89
|
+
* for each conflict and with the Starter's (equal when there are none), in your file's
|
|
90
|
+
* indentation and final newline; null when a side is not a JSON object, to merge by line.
|
|
91
|
+
*/
|
|
92
|
+
export function mergePackageJson(current, base, target) {
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
parsed = [JSON.parse(current), JSON.parse(base), JSON.parse(target)];
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const [yours, before, theirs] = parsed;
|
|
101
|
+
if (!isObject(yours) || !isObject(before) || !isObject(theirs))
|
|
102
|
+
return null;
|
|
103
|
+
const merged = mergeObjects(before, yours, theirs, true);
|
|
104
|
+
const write = (side) => JSON.stringify(resolve(merged, side), null, indentation(current)) + (current.endsWith("\n") ? "\n" : "");
|
|
105
|
+
return { yours: write("yours"), theirs: write("theirs") };
|
|
106
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clubedge/create-clubedge-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"description": "Create React apps from Clubedge Starter with a CLI or local MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.9.0",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
},
|
|
25
25
|
"clubedge": {
|
|
26
26
|
"starterRepository": "Clubedge/clubedge-starter",
|
|
27
|
-
"starterRef": "v0.
|
|
28
|
-
"starterCommit": "
|
|
27
|
+
"starterRef": "v0.29.0",
|
|
28
|
+
"starterCommit": "29350332db4bc44acc1e7b989c04825ff4c0f7b7"
|
|
29
29
|
},
|
|
30
30
|
"license": "Apache-2.0",
|
|
31
31
|
"repository": {
|
|
@@ -33,7 +33,7 @@ jobs:
|
|
|
33
33
|
node-version: 22
|
|
34
34
|
cache: pnpm
|
|
35
35
|
# Apps in other languages run their lint, typecheck, and tests with their own tools.
|
|
36
|
-
- uses: astral-sh/setup-uv@v10.3.
|
|
36
|
+
- uses: astral-sh/setup-uv@v10.3.2
|
|
37
37
|
if: ${{ hashFiles('apps/*/pyproject.toml') != '' }}
|
|
38
38
|
- run: pnpm install --frozen-lockfile
|
|
39
39
|
- run: pnpm format:check
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
// clubedge:if framework=next|tanstack-start
|
|
1
2
|
// Railway's configuration for this project, as code: the migrations before each release and the
|
|
2
3
|
// health check. `pnpm clubedge deploy` applies it before each release; elsewhere, run
|
|
3
4
|
// `railway config apply` once. Railway builds the root Dockerfile; set variables with
|
|
4
5
|
// `railway variable set` or the dashboard.
|
|
6
|
+
// clubedge:end
|
|
7
|
+
// clubedge:if framework=vite|none
|
|
8
|
+
// Railway's configuration for this project, as code. Each app runs as its own Railway service,
|
|
9
|
+
// built from its own Dockerfile, such as apps/api/Dockerfile: `pnpm clubedge deploy` writes that
|
|
10
|
+
// configuration here, with the API's migrations before each release and the health checks, and
|
|
11
|
+
// applies it. Deploy with it: as written, this file describes one service built from a root
|
|
12
|
+
// Dockerfile, which this project doesn't have.
|
|
13
|
+
// clubedge:end
|
|
5
14
|
import { readFileSync } from "node:fs";
|
|
6
15
|
import { defineRailway, project, service } from "railway/iac";
|
|
7
16
|
|
package/template/README.md
CHANGED
|
@@ -38,9 +38,11 @@ Every combination in the CI matrix is generated as a real project and runs its f
|
|
|
38
38
|
|
|
39
39
|
To try the mobile app on an Android phone, download [clubedge-demo.apk](https://github.com/Clubedge/clubedge-starter/releases/latest/download/clubedge-demo.apk) from the latest release and open it, allowing installs from unknown sources when Android asks. It is the app's demo build, with sample data that stays on the phone and no server behind it. iPhones install apps only from the App Store or TestFlight, so there is no file for them.
|
|
40
40
|
|
|
41
|
+
To try the native app, download [clubedge-native-demo.apk](https://github.com/Clubedge/clubedge-starter/releases/latest/download/clubedge-native-demo.apk) the same way. It is the native app of a project without a website, and you sign up and sign in for real against that project's API. The API runs on Render's free plan, so after a quiet spell its first answer takes about a minute. Use a password you don't use elsewhere: the demo's accounts can be deleted at any time.
|
|
42
|
+
|
|
41
43
|
<!-- In generated projects, the block below supplies the title that create-clubedge-app replaces with the project's name; its comment keeps it hidden here. -->
|
|
42
44
|
<!-- clubedge:end -->
|
|
43
|
-
<!-- clubedge:if framework=next|tanstack-start|none
|
|
45
|
+
<!-- clubedge:if framework=next|tanstack-start|vite|none
|
|
44
46
|
# Clubedge Starter
|
|
45
47
|
<!-- clubedge:end -->
|
|
46
48
|
|
|
@@ -104,7 +106,8 @@ To try the mobile app on an Android phone, download [clubedge-demo.apk](https://
|
|
|
104
106
|
- A production image that includes its own migrator (`node /app/migrate.mjs`), for running migrations before each release. <!-- clubedge:only api=none -->
|
|
105
107
|
- Production images for the frontend, served by Node, and for the API, which includes the migrator (`node /app/migrate.mjs`) for running migrations before each release. <!-- clubedge:only api!=none && framework!=none -->
|
|
106
108
|
- A production image for the API, which includes the migrator (`node /app/migrate.mjs`) for running migrations before each release. <!-- clubedge:only framework=none -->
|
|
107
|
-
- A manual deploy workflow for the chosen platform, and a readiness endpoint (`/api/ready`) that checks the database.
|
|
109
|
+
- A manual deploy workflow for the chosen platform, and a readiness endpoint (`/api/ready`) that checks the database. <!-- clubedge:only deploy!=docker -->
|
|
110
|
+
- Deployment with Docker Compose behind Caddy: `pnpm clubedge deploy` writes the Compose file, a Caddyfile, and a workflow that publishes the images to GitHub's container registry with each release. A readiness endpoint (`/api/ready`) checks the database. <!-- clubedge:only deploy=docker -->
|
|
108
111
|
- Deployment to Fly.io with `fly.toml`: migrations run as the release command. <!-- clubedge:only deploy=fly -->
|
|
109
112
|
- Deployment to Railway with `.railway/railway.ts`: migrations run before each release, and `pnpm clubedge deploy` sets up the project. <!-- clubedge:only deploy=railway -->
|
|
110
113
|
- Deployment to Render with a `render.yaml` Blueprint: migrations run before each deploy. <!-- clubedge:only deploy=render -->
|
|
@@ -161,7 +164,7 @@ corepack enable
|
|
|
161
164
|
pnpm install
|
|
162
165
|
```
|
|
163
166
|
|
|
164
|
-
<!-- clubedge:if framework!=none -->
|
|
167
|
+
<!-- clubedge:if framework!=none && infra!=neon -->
|
|
165
168
|
|
|
166
169
|
Copy the example environment file to the web app's local environment file.
|
|
167
170
|
|
|
@@ -178,7 +181,7 @@ cp .env.example apps/web/.env.local
|
|
|
178
181
|
```
|
|
179
182
|
|
|
180
183
|
<!-- clubedge:end -->
|
|
181
|
-
<!-- clubedge:if framework=none -->
|
|
184
|
+
<!-- clubedge:if framework=none && infra!=neon -->
|
|
182
185
|
|
|
183
186
|
Copy the API's example environment file to its local environment file.
|
|
184
187
|
|
|
@@ -226,8 +229,8 @@ Nothing else to install: `pnpm dev` starts a local PostgreSQL database (PGlite,
|
|
|
226
229
|
<!-- clubedge:end -->
|
|
227
230
|
<!-- clubedge:if infra=neon -->
|
|
228
231
|
|
|
229
|
-
Nothing to install: create-clubedge-app created a free [Neon](https://neon.com) database and put its URLs in `apps/web/.env.local`. It lasts 72 hours; claim it into a free Neon account to keep it. Apply the migrations: <!-- clubedge:only framework
|
|
230
|
-
Nothing to install: create-clubedge-app created a free [Neon](https://neon.com) database and put its URLs in `apps/api/.env.local`. It lasts 72 hours; claim it into a free Neon account to keep it. Apply the migrations: <!-- clubedge:only framework=none -->
|
|
232
|
+
Nothing to install: create-clubedge-app created a free [Neon](https://neon.com) database and put its URLs in `apps/web/.env.local`. It lasts 72 hours; claim it into a free Neon account to keep it. Apply the migrations: <!-- clubedge:only framework=next|tanstack-start -->
|
|
233
|
+
Nothing to install: create-clubedge-app created a free [Neon](https://neon.com) database and put its URLs in `apps/api/.env.local`. It lasts 72 hours; claim it into a free Neon account to keep it. Apply the migrations: <!-- clubedge:only framework=vite|none -->
|
|
231
234
|
|
|
232
235
|
```sh
|
|
233
236
|
pnpm db:migrate
|
|
@@ -524,15 +527,21 @@ Shared components are generated under `packages/ui/src/components` and can be im
|
|
|
524
527
|
|
|
525
528
|
Each phase ends with a Starter release and a create-clubedge-app release pinned to it. The full plan is in [ROADMAP.md](ROADMAP.md).
|
|
526
529
|
|
|
527
|
-
| Release
|
|
528
|
-
|
|
|
529
|
-
| v0.5.0 to v0.8.0
|
|
530
|
-
| v0.9.0
|
|
531
|
-
| v0.10.0
|
|
532
|
-
| v0.11.0
|
|
533
|
-
| v0.12.0
|
|
534
|
-
| v0.13.0
|
|
535
|
-
|
|
|
530
|
+
| Release | Theme |
|
|
531
|
+
| ------------------ | ---------------------------------------------------------------------------------------------- |
|
|
532
|
+
| v0.5.0 to v0.8.0 | Optional modules, local services with and without Docker, `add`, `remove`, and `upgrade` |
|
|
533
|
+
| v0.9.0 | Email, password reset, and email verification |
|
|
534
|
+
| v0.10.0 | Deployment to Docker hosts, Fly.io, Railway, Render, Vercel, and Netlify |
|
|
535
|
+
| v0.11.0 | Payments with Stripe |
|
|
536
|
+
| v0.12.0 | Background jobs with pg-boss |
|
|
537
|
+
| v0.13.0 | Observability with OpenTelemetry or Sentry |
|
|
538
|
+
| v0.14.0 to v0.15.0 | Account settings, browser error reporting, and the docs-style dashboard |
|
|
539
|
+
| v0.16.0 to v0.19.0 | Upgrades as pull requests, Railway configuration as code, production variable checks, and Neon |
|
|
540
|
+
| v0.20.0 to v0.21.0 | Pluggable jobs (BullMQ, Inngest) and auth (Firebase, Appwrite), each with a conformance suite |
|
|
541
|
+
| v0.22.0 to v0.26.0 | React + Vite with a separate Hono or FastAPI API, and every module on both |
|
|
542
|
+
| v0.27.0 | Mobile apps with Capacitor |
|
|
543
|
+
| v0.28.0 | The native app with Expo, and projects without a website |
|
|
544
|
+
| next | Proposed: organizations and teams, shipping the native app to stores, and an admin area |
|
|
536
545
|
|
|
537
546
|
## Community
|
|
538
547
|
|
package/template/SETUP.md
CHANGED
|
@@ -17,7 +17,7 @@ If pnpm reports that it ignored a package build script needed by your environmen
|
|
|
17
17
|
|
|
18
18
|
## Configure the local environment
|
|
19
19
|
|
|
20
|
-
<!-- clubedge:if framework!=none -->
|
|
20
|
+
<!-- clubedge:if framework!=none && infra!=neon -->
|
|
21
21
|
|
|
22
22
|
Copy `.env.example` to `apps/web/.env.local`.
|
|
23
23
|
|
|
@@ -34,7 +34,7 @@ cp .env.example apps/web/.env.local
|
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
<!-- clubedge:end -->
|
|
37
|
-
<!-- clubedge:if framework=none -->
|
|
37
|
+
<!-- clubedge:if framework=none && infra!=neon -->
|
|
38
38
|
|
|
39
39
|
Copy `apps/api/.env.example` to `apps/api/.env.local`.
|
|
40
40
|
|
|
@@ -52,6 +52,9 @@ cp apps/api/.env.example apps/api/.env.local
|
|
|
52
52
|
|
|
53
53
|
<!-- clubedge:end -->
|
|
54
54
|
|
|
55
|
+
create-clubedge-app created `apps/web/.env.local` from `.env.example`, with the URLs of the Neon database it created: keep that file, since copying the example over it would remove them. <!-- clubedge:only infra=neon && framework=next|tanstack-start -->
|
|
56
|
+
create-clubedge-app created the local environment files from their examples, and wrote the URLs of the Neon database it created into `apps/api/.env.local`: keep that file, since copying the example over it would remove them. <!-- clubedge:only infra=neon && framework=vite|none -->
|
|
57
|
+
|
|
55
58
|
The example file points at the local services described in [Local services](#local-services) and leaves provider keys blank, so the app builds and starts before anything is configured. <!-- clubedge:only infra!=none -->
|
|
56
59
|
The example file leaves provider URLs and keys blank so the app can build and start without connecting to real services. At minimum, set `DATABASE_URL`. <!-- clubedge:only infra=none -->
|
|
57
60
|
|
|
@@ -65,7 +68,8 @@ The variables are:
|
|
|
65
68
|
- `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY` (for sign-in): see [Supabase Auth](#supabase-auth). <!-- clubedge:only framework=tanstack-start|none && auth=supabase -->
|
|
66
69
|
- `BETTER_AUTH_SECRET` (for sign-in): see [Better Auth](#better-auth). <!-- clubedge:only auth=better-auth && framework!=none -->
|
|
67
70
|
- `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` (for sign-in): see [Better Auth](#better-auth). <!-- clubedge:only auth=better-auth && framework=none -->
|
|
68
|
-
- `FIREBASE_PROJECT_ID`, `FIREBASE_API_KEY`, and in production a service account: see [Firebase Authentication](#firebase-authentication). <!-- clubedge:only auth=firebase -->
|
|
71
|
+
- `FIREBASE_PROJECT_ID`, `FIREBASE_API_KEY`, and in production a service account: see [Firebase Authentication](#firebase-authentication). <!-- clubedge:only auth=firebase && api!=fastapi -->
|
|
72
|
+
- `FIREBASE_PROJECT_ID` and `FIREBASE_API_KEY`: see [Firebase Authentication](#firebase-authentication). <!-- clubedge:only auth=firebase && api=fastapi -->
|
|
69
73
|
- `APPWRITE_ENDPOINT`, `APPWRITE_PROJECT_ID`, and `APPWRITE_API_KEY` (for sign-in): see [Appwrite](#appwrite). <!-- clubedge:only auth=appwrite -->
|
|
70
74
|
- `REDIS_URL` (optional): see [Redis](#redis). <!-- clubedge:only cache=redis -->
|
|
71
75
|
- `SMTP_URL` and `EMAIL_FROM` (for sending email): see [Email](#email). <!-- clubedge:only email=smtp -->
|
|
@@ -151,8 +155,10 @@ These emails are sent by the `send-email` [background job](#background-jobs), so
|
|
|
151
155
|
|
|
152
156
|
## Firebase Authentication
|
|
153
157
|
|
|
154
|
-
[Firebase Authentication](https://firebase.google.com/docs/auth) checks passwords and keeps the accounts; the app's own pages sign users in, and the server holds the session in an HTTP-only cookie (a Firebase session cookie, checked for revocation on each request). Password reset and email confirmation links are generated by Firebase and sent through the [email module](#email), so they open the app's own pages and need no setup in the Firebase console. <!-- clubedge:only email!=none && framework
|
|
155
|
-
[Firebase Authentication](https://firebase.google.com/docs/auth) checks passwords and keeps the accounts; the app's own pages sign users in
|
|
158
|
+
[Firebase Authentication](https://firebase.google.com/docs/auth) checks passwords and keeps the accounts; the app's own pages sign users in, and the server holds the session in an HTTP-only cookie (a Firebase session cookie, checked for revocation on each request). Password reset and email confirmation links are generated by Firebase and sent through the [email module](#email), so they open the app's own pages and need no setup in the Firebase console. <!-- clubedge:only email!=none && framework=next|tanstack-start -->
|
|
159
|
+
[Firebase Authentication](https://firebase.google.com/docs/auth) checks passwords and keeps the accounts; the app's own pages sign users in with Firebase's SDK, which keeps the session in the browser, and send its ID token with each request to the API (`Authorization: Bearer <token>`), which verifies it. Password reset and email confirmation links are generated by Firebase and sent through the [email module](#email), so they open the app's own pages and need no setup in the Firebase console. <!-- clubedge:only email!=none && framework=vite -->
|
|
160
|
+
[Firebase Authentication](https://firebase.google.com/docs/auth) checks passwords and keeps the accounts; the app's own pages sign users in, and the server holds the session in an HTTP-only cookie (a Firebase session cookie, checked for revocation on each request). Without the email module there is no password reset. <!-- clubedge:only email=none && framework=next|tanstack-start -->
|
|
161
|
+
[Firebase Authentication](https://firebase.google.com/docs/auth) checks passwords and keeps the accounts; the app's own pages sign users in with Firebase's SDK, which keeps the session in the browser, and send its ID token with each request to the API (`Authorization: Bearer <token>`), which verifies it. Without the email module there is no password reset. <!-- clubedge:only email=none && framework=vite -->
|
|
156
162
|
[Firebase Authentication](https://firebase.google.com/docs/auth) checks passwords and keeps the accounts. Clients sign users in with Firebase's SDK and send the ID token with each request (`Authorization: Bearer <token>`); the API verifies it, and accepts only confirmed email addresses. <!-- clubedge:only framework=none -->
|
|
157
163
|
|
|
158
164
|
In development, `pnpm auth:dev` starts Firebase's Auth Emulator (`firebase.json`, port 9099), which the example environment points at: no Firebase project or account is needed. Accounts live in memory until it stops. <!-- clubedge:only framework!=none -->
|
|
@@ -162,8 +168,11 @@ In production:
|
|
|
162
168
|
|
|
163
169
|
1. In the [Firebase console](https://console.firebase.google.com), create a project and turn on **Email/Password** under Authentication, Sign-in method.
|
|
164
170
|
2. Set `FIREBASE_PROJECT_ID` and `FIREBASE_API_KEY` (the Web API key, under Project settings, General).
|
|
165
|
-
3. Under Project settings, Service accounts, generate a private key, and set `FIREBASE_CLIENT_EMAIL` and `FIREBASE_PRIVATE_KEY` from it (the key's line breaks as `\n` are fine).
|
|
166
|
-
|
|
171
|
+
3. Under Project settings, Service accounts, generate a private key, and set `FIREBASE_CLIENT_EMAIL` and `FIREBASE_PRIVATE_KEY` from it (the key's line breaks as `\n` are fine). <!-- clubedge:only api!=fastapi -->
|
|
172
|
+
|
|
173
|
+
Remove `FIREBASE_AUTH_EMULATOR_HOST` too: `pnpm check:env` refuses it in production.
|
|
174
|
+
|
|
175
|
+
The FastAPI API needs no service account: it checks each ID token with Firebase's Identity Toolkit, using the Web API key. <!-- clubedge:only api=fastapi -->
|
|
167
176
|
|
|
168
177
|
Changing a password or resetting it signs out every other session; deleting an account removes it from Firebase after the app's own data for the user.
|
|
169
178
|
|
|
@@ -239,10 +248,10 @@ PGlite runs a single database session, so connections take turns. That is plenty
|
|
|
239
248
|
|
|
240
249
|
This project's database is hosted on [Neon](https://neon.com), so there is nothing to install or start:
|
|
241
250
|
|
|
242
|
-
- **PostgreSQL:** create-clubedge-app created a free, unclaimed Neon database and wrote `DATABASE_URL` (pooled) and `DATABASE_URL_DIRECT` into `apps/web/.env.local`. It expires 72 hours after creation and holds up to 100 MB until it is claimed. `pnpm clubedge neon` shows how long it has left. <!-- clubedge:only framework
|
|
243
|
-
- **PostgreSQL:** create-clubedge-app created a free, unclaimed Neon database and wrote `DATABASE_URL` (pooled) and `DATABASE_URL_DIRECT` into `apps/api/.env.local`. It expires 72 hours after creation and holds up to 100 MB until it is claimed. `pnpm clubedge neon` shows how long it has left. <!-- clubedge:only framework=none -->
|
|
244
|
-
- **Keep it:** `pnpm clubedge neon claim` opens Neon's claim page; sign in (free) and the database moves to your account with its data. Claiming resets the password, so copy the new connection strings from the Neon console into `apps/web/.env.local` afterwards: the pooled one into `DATABASE_URL`, the direct one into `DATABASE_URL_DIRECT`. <!-- clubedge:only framework
|
|
245
|
-
- **Keep it:** `pnpm clubedge neon claim` opens Neon's claim page; sign in (free) and the database moves to your account with its data. Claiming resets the password, so copy the new connection strings from the Neon console into `apps/api/.env.local` afterwards: the pooled one into `DATABASE_URL`, the direct one into `DATABASE_URL_DIRECT`. <!-- clubedge:only framework=none -->
|
|
251
|
+
- **PostgreSQL:** create-clubedge-app created a free, unclaimed Neon database and wrote `DATABASE_URL` (pooled) and `DATABASE_URL_DIRECT` into `apps/web/.env.local`. It expires 72 hours after creation and holds up to 100 MB until it is claimed. `pnpm clubedge neon` shows how long it has left. <!-- clubedge:only framework=next|tanstack-start -->
|
|
252
|
+
- **PostgreSQL:** create-clubedge-app created a free, unclaimed Neon database and wrote `DATABASE_URL` (pooled) and `DATABASE_URL_DIRECT` into `apps/api/.env.local`. It expires 72 hours after creation and holds up to 100 MB until it is claimed. `pnpm clubedge neon` shows how long it has left. <!-- clubedge:only framework=vite|none -->
|
|
253
|
+
- **Keep it:** `pnpm clubedge neon claim` opens Neon's claim page; sign in (free) and the database moves to your account with its data. Claiming resets the password, so copy the new connection strings from the Neon console into `apps/web/.env.local` afterwards: the pooled one into `DATABASE_URL`, the direct one into `DATABASE_URL_DIRECT`. <!-- clubedge:only framework=next|tanstack-start -->
|
|
254
|
+
- **Keep it:** `pnpm clubedge neon claim` opens Neon's claim page; sign in (free) and the database moves to your account with its data. Claiming resets the password, so copy the new connection strings from the Neon console into `apps/api/.env.local` afterwards: the pooled one into `DATABASE_URL`, the direct one into `DATABASE_URL_DIRECT`. <!-- clubedge:only framework=vite|none -->
|
|
246
255
|
- **Start over:** `pnpm clubedge neon create` creates a new database and replaces the two URLs, for example after one expired.
|
|
247
256
|
- **Your own project:** paste the connection strings from any Neon project instead; nothing else changes.
|
|
248
257
|
- **Cache:** without `REDIS_URL`, the cache and rate limits use process memory. <!-- clubedge:only cache=redis -->
|
|
@@ -291,7 +300,8 @@ Redis is optional. Set `REDIS_URL` to a TCP connection URL:
|
|
|
291
300
|
REDIS_URL=rediss://default:<password>@<host>:6379
|
|
292
301
|
```
|
|
293
302
|
|
|
294
|
-
Use `rediss://` for TLS connections such as Upstash and `redis://` for an unencrypted connection on a trusted network. The app uses the standard Redis protocol and requires a Node.js runtime with outbound TCP access.
|
|
303
|
+
Use `rediss://` for TLS connections such as Upstash and `redis://` for an unencrypted connection on a trusted network. The app uses the standard Redis protocol and requires a Node.js runtime with outbound TCP access. <!-- clubedge:only api!=fastapi -->
|
|
304
|
+
The API uses the standard Redis protocol and needs outbound TCP access. <!-- clubedge:only api=fastapi -->
|
|
295
305
|
|
|
296
306
|
Without `REDIS_URL`, the rate limiter and cache use process memory. Memory limits apply per server process, so configure Redis when you run several instances or serverless functions. Never commit a real Redis URL or password.
|
|
297
307
|
|
|
@@ -685,7 +695,7 @@ It runs once the repository has the Neon settings below, in Settings, then Secre
|
|
|
685
695
|
|
|
686
696
|
The Starter repository also runs `.github/workflows/stack-previews.yml`. For each pull request, it generates React + Vite with Hono and FastAPI through the CLI, then deploys each generated frontend and API as separate Render web services. Both stacks use the existing main Supabase project's Auth endpoint and publishable key, retrieved by the workflow and passed to their generated frontend and API. Set the Actions secret `SUPABASE_ACCESS_TOKEN` and the repository secret or variable `SUPABASE_PROJECT_REF`. The token needs permission to read API gateway keys. These previews share the main project's Supabase Auth configuration and user pool; their application databases remain isolated on per-stack Neon branches. Also set the existing `RENDER_API_KEY`, `NEON_PROJECT_ID`, and `NEON_API_KEY`; if the Render token can access multiple workspaces, set `RENDER_OWNER_ID`. Connect the Starter GitHub repository to that Render workspace so Render can build generated preview branches. Each stack's Neon branch, API service, frontend service, and generated Git branch are deleted when the pull request closes. To allow Supabase email confirmation and password-reset links to return to the Render frontends, add `https://clubedge-starter-vite-**-web.onrender.com/**` under the Supabase project's Auth URL Configuration > Redirect URLs; [Supabase documents the wildcard format](https://supabase.com/docs/guides/auth/redirect-urls#use-wildcards-in-redirect-urls). Render free services can take about a minute to wake after inactivity. These settings apply only to the Starter repository and are not copied into generated projects. <!-- clubedge:only starter-repository -->
|
|
687
697
|
|
|
688
|
-
From your machine, `pnpm clubedge deploy --preview` does the same for the current Git branch. <!-- clubedge:only deploy=vercel|railway && framework
|
|
698
|
+
From your machine, `pnpm clubedge deploy --preview` does the same for the current Git branch. <!-- clubedge:only deploy=vercel|railway && framework=next|tanstack-start -->
|
|
689
699
|
|
|
690
700
|
<!-- clubedge:end -->
|
|
691
701
|
|
|
@@ -852,13 +862,20 @@ The API is in Python and managed with [uv](https://docs.astral.sh/uv/): its `pac
|
|
|
852
862
|
|
|
853
863
|
### Apps installed on phones
|
|
854
864
|
|
|
855
|
-
An app installed on a phone calls the API and keeps the version it shipped with, often for months. It asks `/api/app-version` which versions the API serves, and the answer is never cached. Set them in `apps/api/.env.local`, or in the API's environment in production; each is `null` while unset:
|
|
865
|
+
An app installed on a phone calls the API and keeps the version it shipped with, often for months. It asks `/api/app-version` which versions the API serves, and the answer is never cached, so a change applies on each app's next check, with no new release of the app. Set them in `apps/api/.env.local`, or in the API's environment in production; each is `null` while unset:
|
|
856
866
|
|
|
857
867
|
- `APP_MINIMUM_VERSION`, the oldest app version the API still serves: below it, the app shows a blocking update screen.
|
|
858
868
|
- `APP_LATEST_VERSION`, the newest released version: below it, the app may show an optional update notice.
|
|
859
869
|
- `APP_UPDATE_URL`, where to get the update: an `https://` link to the store listing or a download page.
|
|
870
|
+
- `APP_NEXT_MINIMUM_VERSION` and `APP_NEXT_MINIMUM_VERSION_FROM`, a minimum announced for a date and time with its time zone, such as `2026-11-01T00:00:00Z`. Until then, apps below it keep working and tell their users to update by that date; from then, the API requires it, with no redeploy.
|
|
860
871
|
|
|
861
|
-
|
|
872
|
+
Each can be set for one platform by adding `_IOS` or `_ANDROID` to its name, such as `APP_UPDATE_URL_IOS` for the App Store listing and `APP_UPDATE_URL_ANDROID` for Google Play, or `APP_LATEST_VERSION_IOS` while App Store review holds a release Google Play already has. The apps say which platform they run on, and the platform's own value wins over the shared one.
|
|
873
|
+
|
|
874
|
+
Versions are `MAJOR.MINOR.PATCH`, such as `1.4.0`. The latest may not be older than the minimum, nor the announced minimum, and the announced minimum must be newer than the current one. Other values fail the request rather than reach the apps, and `pnpm check:env` lists them before a deploy.
|
|
875
|
+
|
|
876
|
+
The apps also send their version (`X-App-Version`) and platform (`X-App-Platform`) with every request. To a request whose version is below the minimum, the API answers `426 update_required`, on every route but `/api/health`, `/api/ready`, and `/api/app-version`, so an app left open for days shows its update screen at its next request, not only when it starts again. Apps built from a Starter before v0.29.0 send no version: the API serves them, and they find the new minimum at their next start. The version is what the app says it is, so this keeps old apps from calling an API they no longer match; it is not a security control.
|
|
877
|
+
|
|
878
|
+
For an urgent fix, raise `APP_MINIMUM_VERSION` itself: the apps below it stop at their next request (older builds, at their next start). Once a security fix is out, don't lower the minimum again, even to roll back a broken release: ship a fixed version instead.
|
|
862
879
|
|
|
863
880
|
Installed apps keep calling the contract they shipped with, so change `openapi.yaml` only in ways they survive: add fields and operations, but never remove or rename them, and never make an optional request field required. The CI workflow's `contract` job compares each pull request's contract with its base branch's using [oasdiff](https://github.com/oasdiff/oasdiff), and fails on a change that breaks existing clients. For a deliberate break before any app has shipped, label the pull request `breaking-contract` and run the job again. Once apps have shipped, a break also needs `APP_MINIMUM_VERSION` raised to the first version that handles it, so older apps ask for an update instead of failing.
|
|
864
881
|
|
|
@@ -872,7 +889,12 @@ Installed apps keep calling the contract they shipped with, so change `openapi.y
|
|
|
872
889
|
|
|
873
890
|
1. Set your app's id in `apps/shell/capacitor.config.ts`: a reverse-domain name you own, such as `com.yourcompany.app`. It becomes permanent once the Android and iOS projects exist and a store lists the app, so set it before the first build. The app's name comes from `apps/web/src/config/site.json`.
|
|
874
891
|
2. Set `WEB_URL` in `apps/shell/.env.local` to the website's public `https://` address, such as `https://app.example.com`. The build refuses any other: the app sends its session there.
|
|
875
|
-
3. Run `pnpm --filter @clubedge/shell android` (or `ios`). It builds the web app for the app, creates `apps/shell/android` (or `apps/shell/ios`) on the first run, copies the build into it, and opens Android Studio (or Xcode), where you run the app on a device or an emulator. Commit `android/` and `ios/`: from then on they are your app's own, with its icons, permissions, and signing settings. The first run also makes the app's icons and splash screens from `apps/shell/assets
|
|
892
|
+
3. Run `pnpm --filter @clubedge/shell android` (or `ios`). It builds the web app for the app, creates `apps/shell/android` (or `apps/shell/ios`) on the first run, copies the build into it, and opens Android Studio (or Xcode), where you run the app on a device or an emulator. Commit `android/` and `ios/`: from then on they are your app's own, with its icons, permissions, and signing settings. The first run also makes the app's icons and splash screens from the art in `apps/shell/assets`, with Capacitor's asset tool (run through npx, so it needs the network then):
|
|
893
|
+
- `logo.svg`, the icon: the white mark on the brand's blue square.
|
|
894
|
+
- `splash.svg` and `splash-dark.svg`, iOS's splash screens: the blue mark in the middle of the light and dark backgrounds.
|
|
895
|
+
- `splash-icon.png`, the blue mark alone, which Android draws in the middle of those backgrounds at 128dp. Android 12 and later show it through a circle 192dp across, which the mark stays inside, and no screen's shape stretches it.
|
|
896
|
+
|
|
897
|
+
After changing them, run `pnpm --filter @clubedge/shell icons`, and set the colors to match at the top of `apps/shell/scripts/icons.mjs`.
|
|
876
898
|
|
|
877
899
|
After changing the web app, `pnpm --filter @clubedge/shell sync` copies a new build into the native projects. Release builds and store listings are made in Android Studio and Xcode, as Capacitor's guides for [Google Play](https://capacitorjs.com/docs/android/deploying-to-google-play) and the [App Store](https://capacitorjs.com/docs/ios/deploying-to-app-store) describe. The project's CI builds a debug APK too; set a `WEB_URL` repository variable so that it calls your website.
|
|
878
900
|
|
|
@@ -901,11 +923,12 @@ Ctrl+C stops it. The installed app keeps loading its pages from your computer un
|
|
|
901
923
|
|
|
902
924
|
### Updates
|
|
903
925
|
|
|
904
|
-
|
|
926
|
+
The app compares its version with the versions the API serves ([Apps installed on phones](#apps-installed-on-phones)) when it starts, when it comes back to the foreground (at most every 15 minutes), and whenever the API answers one of its requests `426 update_required`:
|
|
905
927
|
|
|
906
|
-
- Below `APP_MINIMUM_VERSION`, it shows **Update required**, a screen that keeps the app closed, with an **Update** button that opens `APP_UPDATE_URL` in the phone's browser.
|
|
907
|
-
- Below `
|
|
908
|
-
-
|
|
928
|
+
- Below `APP_MINIMUM_VERSION`, it shows **Update required**, a screen that keeps the app closed, with an **Update** button that opens `APP_UPDATE_URL` in the phone's browser. The app remembers it, so it holds when the app next starts offline, until it is updated.
|
|
929
|
+
- Below `APP_NEXT_MINIMUM_VERSION`, a notice above the page says the date it stops working, with the same button. **Not now** hides it until the next day.
|
|
930
|
+
- Below `APP_LATEST_VERSION` only, a notice offers the update, and **Not now** hides it until a newer version is out.
|
|
931
|
+
- When the API cannot be reached, or does not know `/api/app-version` yet, nothing new shows: a remembered **Update required** still holds, and otherwise the app works as usual.
|
|
909
932
|
|
|
910
933
|
The app's version is its `versionName` in `apps/shell/android/app/build.gradle`, and its version (`MARKETING_VERSION`) in Xcode. Give it as `MAJOR.MINOR.PATCH`, such as `1.4.0`, and raise it with each release; `1.0` counts as `1.0.0`, and a version in another form never sees a notice.
|
|
911
934
|
|
|
@@ -926,7 +949,7 @@ Builds without `--demo` contain none of it. The demo replaces the app's build in
|
|
|
926
949
|
|
|
927
950
|
## The native app
|
|
928
951
|
|
|
929
|
-
`apps/mobile` is a native Android and iOS app, built with [Expo](https://expo.dev) (SDK 58) and React Native, with its own screens: sign-in, a dashboard
|
|
952
|
+
`apps/mobile` is a native Android and iOS app, built with [Expo](https://expo.dev) (SDK 58) and React Native, with its own screens: a welcome screen and sign-in, then two tabs, a dashboard and the settings, with the user's profile. <!-- clubedge:only auth!=none -->
|
|
930
953
|
`apps/mobile` is a native Android and iOS app, built with [Expo](https://expo.dev) (SDK 58) and React Native, with its own screens. <!-- clubedge:only auth=none -->
|
|
931
954
|
It calls the API through your website, as the website does, with the same contract client (`@clubedge/contract`). Expo generates its Android and iOS projects from `apps/mobile/app.config.ts` whenever it builds them (`expo prebuild`), so `android/` and `ios/` are not committed, and upgrades only change the app's TypeScript and config. <!-- clubedge:only framework!=none -->
|
|
932
955
|
It calls the API directly, with the contract client (`@clubedge/contract`). Expo generates its Android and iOS projects from `apps/mobile/app.config.ts` whenever it builds them (`expo prebuild`), so `android/` and `ios/` are not committed, and upgrades only change the app's TypeScript and config. <!-- clubedge:only framework=none -->
|
|
@@ -970,11 +993,14 @@ How the app works with the API: <!-- clubedge:only framework=none -->
|
|
|
970
993
|
- Emailed links, such as sign-up confirmations and password resets, open the website. After confirming an address or setting a new password, sign in again in the app. <!-- clubedge:only auth!=none && framework!=none -->
|
|
971
994
|
- Emailed links open the app, through its address scheme: confirming a new account opens its sign-in screen, and a password reset link opens its reset screen, which sets the new password. <!-- clubedge:only auth=better-auth && email!=none && framework=none -->
|
|
972
995
|
- Supabase's emailed links open the app, through its address scheme, once its Redirect URLs list it (see [Supabase Auth](#supabase-auth)): confirming a new account opens the sign-in screen, and a password reset link opens the reset screen, which sets the new password. <!-- clubedge:only auth=supabase && framework=none -->
|
|
973
|
-
-
|
|
996
|
+
- The Settings tab holds the account: the display name, a Dark mode switch that holds over the phone's appearance, the password, deleting the account (as the App Store requires of apps that let people sign up), and signing out of this device or of all of them. <!-- clubedge:only auth!=none -->
|
|
974
997
|
- The app has no billing: app stores require their own in-app purchases for digital goods, so subscriptions stay on the website. <!-- clubedge:only payments!=none -->
|
|
975
998
|
- Its icon, Android's adaptive icon, and its splash screen are the images in `apps/mobile/assets`, which `app.config.ts` names.
|
|
999
|
+
- Its look is in `apps/mobile/src/lib/theme.ts`: its colors, in light and dark, and its type scale, in Inter, which loads while the splash screen shows.
|
|
1000
|
+
- The welcome screen shows the brand's blue and the app's mark. For a photo of your own there, add it to `apps/mobile/assets` and set `heroPhoto` in `apps/mobile/src/app/welcome.tsx`. <!-- clubedge:only auth!=none -->
|
|
1001
|
+
- The welcome and sign-up screens link to your Terms and Privacy Policy: set their addresses, `links.terms` and `links.privacy` in `apps/mobile/src/config/site.json`, before the first release. The stores ask for a privacy policy too. <!-- clubedge:only auth!=none -->
|
|
976
1002
|
|
|
977
|
-
|
|
1003
|
+
The app compares its version, `version` in `app.config.ts` (`MAJOR.MINOR.PATCH`; raise it with each release), with the versions the API serves ([Apps installed on phones](#apps-installed-on-phones)) when it starts, when it comes back to the foreground (at most every 15 minutes), and whenever the API answers one of its requests `426 update_required`. Below `APP_MINIMUM_VERSION`, it shows only **Update to continue**, with the installed and required versions and an **Update now** button that opens `APP_UPDATE_URL`, and remembers it, so it holds when the app next starts offline, until it is updated. Below `APP_NEXT_MINIMUM_VERSION`, a sheet over any screen offers the update, with the date this version stops working and the days left; **Not now** hides it until the next day. Below `APP_LATEST_VERSION`, a banner on Home offers the update, and its close button hides it until a newer version is out. All of them are in `src/components/update-gate.tsx`. The logic is shared with the API in `packages/contract` (`app-version.ts` and `app-update.ts`).
|
|
978
1004
|
|
|
979
1005
|
<!-- clubedge:end -->
|
|
980
1006
|
|