@clubedge/create-clubedge-app 0.5.0 → 0.6.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 (39) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +12 -10
  3. package/dist/cli/args.js +2 -1
  4. package/dist/cli/ui.js +1 -0
  5. package/dist/core/customize.js +7 -1
  6. package/dist/core/manifest.js +1 -0
  7. package/dist/core/plan.js +15 -1
  8. package/dist/core/source.js +12 -9
  9. package/dist/index.js +1 -1
  10. package/package.json +3 -3
  11. package/template/.env.example +24 -10
  12. package/template/README.md +38 -1
  13. package/template/SETUP.md +43 -1
  14. package/template/_gitignore +3 -0
  15. package/template/apps/start/.env.example +24 -10
  16. package/template/apps/start/package.json +4 -2
  17. package/template/apps/start/scripts/check-services.ts +106 -0
  18. package/template/apps/web/package.json +5 -3
  19. package/template/apps/web/scripts/check-services.ts +106 -0
  20. package/template/clubedge.template.json +34 -1
  21. package/template/docker-compose.dev.yml +42 -0
  22. package/template/package.json +7 -2
  23. package/template/packages/auth/package.json +1 -1
  24. package/template/packages/auth-better-auth/package.json +1 -1
  25. package/template/packages/auth-supabase/package.json +1 -1
  26. package/template/packages/cache/package.json +1 -1
  27. package/template/packages/cache-redis/package.json +1 -1
  28. package/template/packages/core/package.json +1 -1
  29. package/template/packages/db/package.json +1 -1
  30. package/template/packages/storage/package.json +1 -1
  31. package/template/packages/storage-s3/package.json +1 -1
  32. package/template/packages/storage-s3/src/index.test.ts +22 -0
  33. package/template/packages/storage-s3/src/index.ts +7 -1
  34. package/template/packages/storage-supabase/package.json +1 -1
  35. package/template/packages/ui/package.json +1 -1
  36. package/template/playwright.config.ts +3 -1
  37. package/template/pnpm-lock.yaml +6 -0
  38. package/template/supabase/config.toml +40 -0
  39. package/template.lock.json +6 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to `create-clubedge-app` are documented here.
4
4
 
5
+ ## 0.6.0
6
+
7
+ - Default to Starter `v0.6.0`, which adds the local infrastructure module and `pnpm check:services`.
8
+ - Add `--infra docker|supabase|none`: Docker Compose services, the Supabase CLI's local stack, or no local services. The environment examples point at the chosen services.
9
+ - Let module options set root `package.json` scripts; scripts that only left-out options define are removed, so each project gets the `infra:*` scripts for its option.
10
+ - Fix Starter tag verification on Windows, where an annotated tag resolved to the tag object instead of its commit and scaffolding a downloaded Starter release failed.
11
+
5
12
  ## 0.5.0
6
13
 
7
14
  - Default to Starter `v0.5.0`, which adds optional modules, Better Auth, and template schema 3.
package/README.md CHANGED
@@ -12,27 +12,27 @@ The CLI downloads a tested Starter release, names the generated project, creates
12
12
  Each CLI release is tested against one specific Starter release:
13
13
 
14
14
  ```text
15
- create-clubedge-app v0.5.0
15
+ create-clubedge-app v0.6.0
16
16
  |
17
17
  v
18
- Clubedge Starter v0.5.0
19
- commit d9375c2c3b520323ff0255ef8ec65da19251c3c1
18
+ Clubedge Starter v0.6.0
19
+ commit bf5a5e801254816455cbb837902283b788ab7595
20
20
  ```
21
21
 
22
22
  The current relationship is:
23
23
 
24
24
  | CLI release | Default Starter release | Meaning |
25
25
  | --- | --- | --- |
26
- | `create-clubedge-app@0.5.0` | `clubedge-starter@v0.5.0` | The CLI's required, tested default |
26
+ | `create-clubedge-app@0.6.0` | `clubedge-starter@v0.6.0` | The CLI's required, tested default |
27
27
 
28
28
  This means the same CLI version always scaffolds the same Starter revision by default. The CLI does not silently follow Starter `main`.
29
29
 
30
- The `v0.5.0` tag is expected to resolve to commit
31
- `d9375c2c3b520323ff0255ef8ec65da19251c3c1`. The CLI verifies this before
30
+ The `v0.6.0` tag is expected to resolve to commit
31
+ `bf5a5e801254816455cbb837902283b788ab7595`. The CLI verifies this before
32
32
  scaffolding with the default reference and fails rather than silently using a
33
33
  moved tag.
34
34
 
35
- When the Starter changes, its maintainers first publish a new versioned release, such as `v0.5.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.
35
+ When the Starter changes, its maintainers first publish a new versioned release, such as `v0.6.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.
36
36
 
37
37
  ## Requirements
38
38
 
@@ -92,6 +92,7 @@ Authentication, file storage, and caching are modules. Each has a default, and a
92
92
  | `--auth` | `supabase`, `better-auth`, `none` | `supabase` |
93
93
  | `--storage` | `s3`, `supabase` (needs Supabase Auth), `none` | `s3` |
94
94
  | `--cache` | `redis` (falls back to memory without `REDIS_URL`), `memory` | `redis` |
95
+ | `--infra` | `docker` (Docker Compose), `supabase` (Supabase CLI), `none` | `docker` |
95
96
 
96
97
  ```sh
97
98
  # Only the monorepo, UI, database, and tooling
@@ -109,6 +110,7 @@ Usage: pnpm dlx @clubedge/create-clubedge-app [project-directory] [options]
109
110
  --auth <id> Authentication: supabase (default), better-auth, or none
110
111
  --storage <id> File storage: s3 (default), supabase, or none
111
112
  --cache <id> Cache and rate limits: redis (default) or memory
113
+ --infra <id> Local services: docker (default), supabase, or none
112
114
  --ref <ref> Download this Starter tag, branch, or commit from GitHub instead
113
115
  --template-dir <path> Scaffold from a local Starter checkout (for Starter development)
114
116
  --dry-run Show what would be created without writing anything
@@ -134,7 +136,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --yes --no-git --framework tanstac
134
136
  pnpm dlx @clubedge/create-clubedge-app my-app --ref main
135
137
 
136
138
  # Reproduce a specific Starter release or commit
137
- pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.5.0
139
+ pnpm dlx @clubedge/create-clubedge-app my-app --ref v0.6.0
138
140
  pnpm dlx @clubedge/create-clubedge-app my-app --ref <commit-sha>
139
141
 
140
142
  # Test local Starter changes before releasing them
@@ -145,7 +147,7 @@ pnpm dlx @clubedge/create-clubedge-app my-app --template-dir ../clubedge-starter
145
147
 
146
148
  Each CLI release bundles one Starter release. When the release is built, `scripts/bundle-template.mjs` clones the pinned Starter tag, verifies that it resolves to the pinned commit, and copies its tracked files into the package (`template/` and `template.lock.json`). Running the CLI then needs no GitHub access for the default Starter.
147
149
 
148
- The Starter describes itself in `clubedge.template.json`: its app directory, site config, environment files, Docker image name, and files that stay out of generated projects. Schema 2 manifests also list each framework's app directory, environment example, and Dockerfile. The CLI moves the selected framework's app to `apps/web` (renaming its package to `@clubedge/web`), moves its environment example and Dockerfile to the project root, leaves the other frameworks out, and removes their entries from `pnpm-lock.yaml` so `pnpm install --frozen-lockfile` keeps working. Schema 3 adds `modules`: for each option, the workspace packages it uses, the files it owns, the variant files that replace defaults, and the options it requires. Files listed under `conditional` contain `clubedge:if <condition>` / `clubedge:end` blocks (in `#`, `//`, `<!-- -->`, or JSX comments) that the CLI keeps or drops for the selection, always removing the markers; `clubedge:if starter-repository` marks sections for Starter maintainers only. The CLI refuses a Starter whose manifest schema is newer than it understands and asks you to update. Starters before v0.3.0 have no manifest and use the previous layout.
150
+ The Starter describes itself in `clubedge.template.json`: its app directory, site config, environment files, Docker image name, and files that stay out of generated projects. Schema 2 manifests also list each framework's app directory, environment example, and Dockerfile. The CLI moves the selected framework's app to `apps/web` (renaming its package to `@clubedge/web`), moves its environment example and Dockerfile to the project root, leaves the other frameworks out, and removes their entries from `pnpm-lock.yaml` so `pnpm install --frozen-lockfile` keeps working. Schema 3 adds `modules`: for each option, the workspace packages it uses, the files it owns, the variant files that replace defaults, the root `package.json` scripts it sets, and the options it requires. Files listed under `conditional` contain `clubedge:if <condition>` / `clubedge:end` blocks (in `#`, `//`, `<!-- -->`, or JSX comments) that the CLI keeps or drops for the selection, always removing the markers; `clubedge:if starter-repository` marks sections for Starter maintainers only. The CLI refuses a Starter whose manifest schema is newer than it understands and asks you to update. Starters before v0.3.0 have no manifest and use the previous layout.
149
151
 
150
152
  The CLI first builds a plan (the files to copy and the edits to make), which `--dry-run` prints, and then applies it. The source is organized the same way:
151
153
 
@@ -179,7 +181,7 @@ CI packs the CLI with `npm pack`, scaffolds a clean project from the packed tarb
179
181
  1. Make and validate changes in `clubedge-starter`.
180
182
  2. Run lint, typecheck, unit tests, browser tests, and production build.
181
183
  3. Update the Starter package versions.
182
- 4. Commit `main`, create a release tag such as `v0.5.0`, and publish the Starter release.
184
+ 4. Commit `main`, create a release tag such as `v0.6.0`, and publish the Starter release.
183
185
 
184
186
  ### CLI maintainers
185
187
 
package/dist/cli/args.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { parseArgs } from "node:util";
2
2
  /** Module flags the CLI accepts. Which options exist is up to the Starter's manifest. */
3
- export const MODULE_FLAGS = ["auth", "storage", "cache"];
3
+ export const MODULE_FLAGS = ["auth", "storage", "cache", "infra"];
4
4
  export class UsageError extends Error {
5
5
  name = "UsageError";
6
6
  }
@@ -17,6 +17,7 @@ export function parseCliArgs(argv) {
17
17
  auth: { type: "string" },
18
18
  storage: { type: "string" },
19
19
  cache: { type: "string" },
20
+ infra: { type: "string" },
20
21
  ref: { type: "string" },
21
22
  "template-dir": { type: "string" },
22
23
  install: { type: "boolean", default: true },
package/dist/cli/ui.js CHANGED
@@ -67,6 +67,7 @@ Options:
67
67
  --auth <id> Authentication: supabase (default), better-auth, or none
68
68
  --storage <id> File storage: s3 (default), supabase, or none
69
69
  --cache <id> Cache and rate limits: redis (default) or memory
70
+ --infra <id> Local services: docker (default), supabase, or none
70
71
  --ref <ref> Download this Starter tag, branch, or commit from GitHub instead
71
72
  --template-dir <path> Scaffold from a local Starter checkout (for Starter development)
72
73
  --dry-run Show what would be created without writing anything
@@ -1,9 +1,15 @@
1
1
  // Pure text transforms applied to template files. Keeping them free of I/O makes each one easy
2
2
  // to test and lets --dry-run describe them without touching the disk.
3
3
  const json = (value) => `${JSON.stringify(value, null, 2)}\n`;
4
- export function customizeRootManifest(source, { packageName }, provenance, dockerImage) {
4
+ export function customizeRootManifest(source, { packageName }, provenance, dockerImage, scripts = { set: {}, remove: [] }) {
5
5
  const manifest = JSON.parse(source);
6
6
  manifest.name = packageName;
7
+ if (manifest.scripts && typeof manifest.scripts === "object") {
8
+ for (const name of scripts.remove)
9
+ delete manifest.scripts[name];
10
+ }
11
+ if (Object.keys(scripts.set).length)
12
+ manifest.scripts = { ...manifest.scripts, ...scripts.set };
7
13
  manifest.clubedge = {
8
14
  cliVersion: provenance.cliVersion,
9
15
  starterRepository: provenance.starterRepository,
@@ -102,6 +102,7 @@ function parseModules(value) {
102
102
  files: optionalStringArray(option, "files", optionPrefix),
103
103
  replace: optionalStringRecord(option, "replace", optionPrefix),
104
104
  requires: parseRequires(option.requires, optionPrefix),
105
+ scripts: optionalStringRecord(option, "scripts", optionPrefix),
105
106
  };
106
107
  }
107
108
  const defaultOption = requireString(entry, "default", prefix);
package/dist/core/plan.js CHANGED
@@ -97,8 +97,22 @@ function resolveModuleFiles(manifest, selection) {
97
97
  }
98
98
  }
99
99
  }
100
+ // Selected options set their scripts; scripts only left-out options define are removed.
101
+ const scripts = { set: {}, remove: [] };
102
+ const offeredScripts = new Set();
103
+ for (const [id, module] of Object.entries(manifest.modules)) {
104
+ for (const [optionId, option] of Object.entries(module.options)) {
105
+ for (const [name, command] of Object.entries(option.scripts)) {
106
+ offeredScripts.add(name);
107
+ if (selection[id] === optionId)
108
+ scripts.set[name] = command;
109
+ }
110
+ }
111
+ }
112
+ scripts.remove = [...offeredScripts].filter((name) => !(name in scripts.set));
100
113
  const removedPackages = [...offeredPackages].filter(([name]) => !usedPackages.has(name));
101
114
  return {
115
+ scripts,
102
116
  removedPackageNames: removedPackages.map(([name]) => name),
103
117
  removedPackageDirectories: removedPackages.map(([, directory]) => directory),
104
118
  replacedTargets: new Set(replacements.values()),
@@ -165,7 +179,7 @@ export function createPlan(input) {
165
179
  edits.push({
166
180
  path: "package.json",
167
181
  description: `Name the package "${identity.packageName}" and record its origin`,
168
- apply: (text) => customizeRootManifest(text, identity, provenance, manifest.dockerImage),
182
+ apply: (text) => customizeRootManifest(text, identity, provenance, manifest.dockerImage, moduleFiles.scripts),
169
183
  });
170
184
  }
171
185
  const appManifest = `${manifest.app}/package.json`;
@@ -50,15 +50,18 @@ export async function readDirectoryTemplate(directory) {
50
50
  }
51
51
  /** Resolves a tag to its commit with `git ls-remote`, following annotated tags. */
52
52
  export async function resolveTagCommit(repository, tag) {
53
- const output = await runCapture("git", [
54
- "ls-remote",
55
- `https://github.com/${repository}.git`,
56
- `refs/tags/${tag}^{}`,
57
- `refs/tags/${tag}`,
58
- ]);
59
- const lines = output.split("\n").map((line) => line.split(/\s+/));
60
- const peeled = lines.find(([, ref]) => ref?.endsWith("^{}"));
61
- return (peeled ?? lines[0])?.[0] || undefined;
53
+ // List every tag rather than passing a `refs/tags/<tag>^{}` pattern: on Windows the command
54
+ // runs through cmd.exe, which strips `^`, so the peeled ref would never match.
55
+ const output = await runCapture("git", ["ls-remote", "--tags", `https://github.com/${repository}.git`]);
56
+ return pickTagCommit(output, tag);
57
+ }
58
+ /** Picks a tag's commit from `git ls-remote` output, preferring the peeled ref of an annotated tag. */
59
+ export function pickTagCommit(output, tag) {
60
+ const refs = new Map(output
61
+ .split("\n")
62
+ .map((line) => line.trim().split(/\s+/))
63
+ .map(([id, ref]) => [ref, id]));
64
+ return refs.get(`refs/tags/${tag}^{}`) ?? refs.get(`refs/tags/${tag}`);
62
65
  }
63
66
  export async function downloadGitHubTemplate(ref, { repository = starterPin.starterRepository, expectedCommit } = {}) {
64
67
  if (expectedCommit) {
package/dist/index.js CHANGED
@@ -61,7 +61,7 @@ async function chooseModules(manifest, requested, interactive) {
61
61
  if (!interactive || unasked.length === 0)
62
62
  return requested;
63
63
  const customize = await confirm({
64
- message: "Customize the included services (auth, storage, cache)?",
64
+ message: "Customize the included services (auth, storage, cache, local infrastructure)?",
65
65
  initialValue: false,
66
66
  });
67
67
  if (isCancel(customize))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/create-clubedge-app",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Create a Next.js or TanStack Start application from the Clubedge Starter reference implementation.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.9.0",
@@ -22,8 +22,8 @@
22
22
  },
23
23
  "clubedge": {
24
24
  "starterRepository": "Clubedge/clubedge-starter",
25
- "starterRef": "v0.5.0",
26
- "starterCommit": "d9375c2c3b520323ff0255ef8ec65da19251c3c1"
25
+ "starterRef": "v0.6.0",
26
+ "starterCommit": "bf5a5e801254816455cbb837902283b788ab7595"
27
27
  },
28
28
  "license": "Apache-2.0",
29
29
  "repository": {
@@ -1,12 +1,18 @@
1
1
  NODE_ENV=development
2
2
  NEXT_PUBLIC_APP_URL=http://localhost:3000
3
3
 
4
- # Supabase PostgreSQL: use the transaction pooler with prepare disabled (configured in packages/db).
5
- DATABASE_URL=postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres
4
+ # Supabase PostgreSQL: use the transaction pooler with prepare disabled (configured in packages/db). # clubedge:only infra=none
5
+ DATABASE_URL=postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres # clubedge:only infra=none
6
+ # Local Supabase database, started by pnpm infra:up. # clubedge:only infra=supabase
7
+ DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres # clubedge:only infra=supabase
8
+ # Local PostgreSQL from docker-compose.dev.yml, started by pnpm infra:up. # clubedge:only infra=docker
9
+ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/app # clubedge:only infra=docker
6
10
 
7
11
  # clubedge:if auth=supabase
8
12
  # Supabase Auth (publishable key only; never put a service role key in the browser)
9
- NEXT_PUBLIC_SUPABASE_URL=
13
+ NEXT_PUBLIC_SUPABASE_URL= # clubedge:only infra!=supabase
14
+ # Local Supabase Auth. pnpm infra:status prints the publishable key to copy below. # clubedge:only infra=supabase
15
+ NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321 # clubedge:only infra=supabase
10
16
  NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
11
17
  # clubedge:end
12
18
 
@@ -19,20 +25,28 @@ BETTER_AUTH_SECRET=
19
25
  # clubedge:if cache=redis
20
26
  # Optional cache and rate limits (Redis protocol; rediss:// enables TLS). Without it, both use
21
27
  # process memory. Upstash example: rediss://default:YOUR_PASSWORD@your-database.upstash.io:6379
22
- REDIS_URL=
28
+ REDIS_URL= # clubedge:only infra!=docker
29
+ REDIS_URL=redis://localhost:6379 # clubedge:only infra=docker
23
30
  # clubedge:end
24
31
 
25
32
  # clubedge:if storage=s3
26
33
  # S3-compatible storage (AWS S3, Cloudflare R2, MinIO)
27
- STORAGE_BUCKET=
28
- STORAGE_REGION=auto
29
- STORAGE_ENDPOINT=
30
- STORAGE_ACCESS_KEY_ID=
31
- STORAGE_SECRET_ACCESS_KEY=
34
+ STORAGE_BUCKET= # clubedge:only infra!=docker
35
+ STORAGE_REGION=auto # clubedge:only infra!=docker
36
+ STORAGE_ENDPOINT= # clubedge:only infra!=docker
37
+ STORAGE_ACCESS_KEY_ID= # clubedge:only infra!=docker
38
+ STORAGE_SECRET_ACCESS_KEY= # clubedge:only infra!=docker
39
+ # Local S3-compatible storage from docker-compose.dev.yml; it accepts any credentials. # clubedge:only infra=docker
40
+ STORAGE_BUCKET=app # clubedge:only infra=docker
41
+ STORAGE_REGION=us-east-1 # clubedge:only infra=docker
42
+ STORAGE_ENDPOINT=http://localhost:9090 # clubedge:only infra=docker
43
+ STORAGE_ACCESS_KEY_ID=local # clubedge:only infra=docker
44
+ STORAGE_SECRET_ACCESS_KEY=local-secret # clubedge:only infra=docker
32
45
  STORAGE_PUBLIC_URL=
33
46
  # clubedge:end
34
47
 
35
48
  # clubedge:if storage=supabase
36
49
  # Supabase Storage bucket, accessed with the signed-in user's Supabase session
37
- SUPABASE_STORAGE_BUCKET=
50
+ SUPABASE_STORAGE_BUCKET= # clubedge:only infra!=supabase
51
+ SUPABASE_STORAGE_BUCKET=app # clubedge:only infra=supabase
38
52
  # clubedge:end
@@ -93,7 +93,36 @@ macOS/Linux:
93
93
  cp .env.example apps/web/.env.local
94
94
  ```
95
95
 
96
- Set `DATABASE_URL` in `apps/web/.env.local`, then start the app. [SETUP.md](SETUP.md) describes every variable and provider.
96
+ <!-- clubedge:if infra=docker -->
97
+
98
+ Start PostgreSQL and the other local services with Docker, apply the migrations, and check that the app can reach every service. The example environment already points at them.
99
+
100
+ ```sh
101
+ pnpm infra:up
102
+ pnpm db:migrate
103
+ pnpm check:services
104
+ ```
105
+
106
+ <!-- clubedge:end -->
107
+ <!-- clubedge:if infra=supabase -->
108
+
109
+ Start the local Supabase stack (it needs Docker), copy the publishable key that `pnpm infra:status` prints into `apps/web/.env.local`, and apply the migrations.
110
+
111
+ ```sh
112
+ pnpm infra:up
113
+ pnpm infra:status
114
+ pnpm db:migrate
115
+ pnpm check:services
116
+ ```
117
+
118
+ <!-- clubedge:end -->
119
+ <!-- clubedge:if infra=none -->
120
+
121
+ Set `DATABASE_URL` and your providers' settings in `apps/web/.env.local`, then run `pnpm db:migrate` and `pnpm check:services`.
122
+
123
+ <!-- clubedge:end -->
124
+
125
+ Then start the app. [SETUP.md](SETUP.md) describes every variable and provider.
97
126
 
98
127
  ```sh
99
128
  pnpm dev
@@ -125,6 +154,14 @@ Run these from the repository root:
125
154
 
126
155
  Playwright's first run may require installing Chromium with `pnpm exec playwright install chromium`. CI runs the browser checks automatically.
127
156
 
157
+ Local services:
158
+
159
+ - `pnpm check:services`: check every configured service with a real round trip.
160
+ - `pnpm infra:up` and `pnpm infra:down`: start and stop the local services from `docker-compose.dev.yml`. <!-- clubedge:only infra=docker -->
161
+ - `pnpm infra:reset`: stop them and delete their data. `pnpm infra:logs` follows their logs. <!-- clubedge:only infra=docker -->
162
+ - `pnpm infra:up` and `pnpm infra:down`: start and stop the local Supabase stack. <!-- clubedge:only infra=supabase -->
163
+ - `pnpm infra:status`: print its URLs and keys. `pnpm infra:reset` stops it and deletes its data. <!-- clubedge:only infra=supabase -->
164
+
128
165
  <!-- clubedge:if starter-repository -->
129
166
 
130
167
  ## Working on this repository
package/template/SETUP.md CHANGED
@@ -31,7 +31,10 @@ macOS/Linux:
31
31
  cp .env.example apps/web/.env.local
32
32
  ```
33
33
 
34
- 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`. The variables are:
34
+ 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 -->
35
+ 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 -->
36
+
37
+ The variables are:
35
38
 
36
39
  - `DATABASE_URL` (required): PostgreSQL connection URL, such as a Supabase transaction pooler URL.
37
40
  - `NEXT_PUBLIC_APP_URL` (recommended): the app's origin, defaulting to `http://localhost:3000`. <!-- clubedge:only framework=next -->
@@ -83,6 +86,45 @@ Put the result in `BETTER_AUTH_SECRET`. Until it is set, the dashboard stays vie
83
86
 
84
87
  <!-- clubedge:end -->
85
88
 
89
+ ## Local services
90
+
91
+ <!-- clubedge:if infra=docker -->
92
+
93
+ `docker-compose.dev.yml` runs the services this project uses on your machine, listening on `127.0.0.1` only. Install Docker Desktop (or Docker Engine with Compose), then:
94
+
95
+ ```sh
96
+ pnpm infra:up # start and wait until every service is healthy
97
+ pnpm db:migrate # create the tables
98
+ pnpm check:services
99
+ ```
100
+
101
+ - PostgreSQL 17 on port 5432, database `app`, user and password `postgres`. Data survives restarts in a Docker volume.
102
+ - Redis on port 6379. <!-- clubedge:only cache=redis -->
103
+ - S3-compatible storage (adobe/s3mock) on port 9090 with the bucket `app`. It accepts any credentials and is for development only. <!-- clubedge:only storage=s3 -->
104
+
105
+ `pnpm infra:down` stops the services, `pnpm infra:reset` also deletes their data, and `pnpm infra:logs` follows their logs. The file is for development; in production, point the same variables at managed services.
106
+
107
+ <!-- clubedge:end -->
108
+ <!-- clubedge:if infra=supabase -->
109
+
110
+ `supabase/config.toml` configures the Supabase CLI's local stack: PostgreSQL, Auth, Storage, the Studio dashboard, and an inbox that captures emails. The CLI runs it in Docker, so install Docker Desktop first. Then:
111
+
112
+ ```sh
113
+ pnpm infra:up # start the stack (the first run downloads its images)
114
+ pnpm infra:status # print the local URLs and keys
115
+ pnpm db:migrate
116
+ pnpm check:services
117
+ ```
118
+
119
+ The database is on port 54322 and the API on port 54321, matching the example environment. Copy the publishable key from `pnpm infra:status` into the Supabase variables. Email confirmation is off locally, so sign-up signs the user in directly. `pnpm infra:down` stops the stack and `pnpm infra:reset` also deletes its data.
120
+
121
+ <!-- clubedge:end -->
122
+ <!-- clubedge:if infra=none -->
123
+
124
+ This project does not include local service definitions. Point `DATABASE_URL` and the provider variables at services you run yourself or at hosted ones, such as Supabase or Neon for PostgreSQL and Upstash for Redis, then run `pnpm check:services` to confirm the app can reach them.
125
+
126
+ <!-- clubedge:end -->
127
+
86
128
  ## Set up the database
87
129
 
88
130
  Application tables are managed with Drizzle. Keep one migration source of truth for application tables: do not also create an independent Supabase migration history for those same tables.
@@ -17,3 +17,6 @@ test-results/
17
17
  dist/
18
18
  # Generated by TanStack Router during dev, build, and typecheck
19
19
  routeTree.gen.ts
20
+ # Supabase CLI working files
21
+ supabase/.temp/
22
+ supabase/.branches/
@@ -2,13 +2,19 @@
2
2
  # development JSX that crashes the production server. `vite dev` and `vite build` set it.
3
3
  APP_URL=http://localhost:3000
4
4
 
5
- # Supabase PostgreSQL: use the transaction pooler with prepare disabled (configured in packages/db).
6
- DATABASE_URL=postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres
5
+ # Supabase PostgreSQL: use the transaction pooler with prepare disabled (configured in packages/db). # clubedge:only infra=none
6
+ DATABASE_URL=postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres # clubedge:only infra=none
7
+ # Local Supabase database, started by pnpm infra:up. # clubedge:only infra=supabase
8
+ DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres # clubedge:only infra=supabase
9
+ # Local PostgreSQL from docker-compose.dev.yml, started by pnpm infra:up. # clubedge:only infra=docker
10
+ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/app # clubedge:only infra=docker
7
11
 
8
12
  # clubedge:if auth=supabase
9
13
  # Supabase Auth (publishable key only; never use a service role key here).
10
14
  # All auth runs on the server, so these need no browser prefix.
11
- SUPABASE_URL=
15
+ SUPABASE_URL= # clubedge:only infra!=supabase
16
+ # Local Supabase Auth. pnpm infra:status prints the publishable key to copy below. # clubedge:only infra=supabase
17
+ SUPABASE_URL=http://127.0.0.1:54321 # clubedge:only infra=supabase
12
18
  SUPABASE_PUBLISHABLE_KEY=
13
19
  # clubedge:end
14
20
 
@@ -21,20 +27,28 @@ BETTER_AUTH_SECRET=
21
27
  # clubedge:if cache=redis
22
28
  # Optional cache and rate limits (Redis protocol; rediss:// enables TLS). Without it, both use
23
29
  # process memory. Upstash example: rediss://default:YOUR_PASSWORD@your-database.upstash.io:6379
24
- REDIS_URL=
30
+ REDIS_URL= # clubedge:only infra!=docker
31
+ REDIS_URL=redis://localhost:6379 # clubedge:only infra=docker
25
32
  # clubedge:end
26
33
 
27
34
  # clubedge:if storage=s3
28
35
  # S3-compatible storage (AWS S3, Cloudflare R2, MinIO)
29
- STORAGE_BUCKET=
30
- STORAGE_REGION=auto
31
- STORAGE_ENDPOINT=
32
- STORAGE_ACCESS_KEY_ID=
33
- STORAGE_SECRET_ACCESS_KEY=
36
+ STORAGE_BUCKET= # clubedge:only infra!=docker
37
+ STORAGE_REGION=auto # clubedge:only infra!=docker
38
+ STORAGE_ENDPOINT= # clubedge:only infra!=docker
39
+ STORAGE_ACCESS_KEY_ID= # clubedge:only infra!=docker
40
+ STORAGE_SECRET_ACCESS_KEY= # clubedge:only infra!=docker
41
+ # Local S3-compatible storage from docker-compose.dev.yml; it accepts any credentials. # clubedge:only infra=docker
42
+ STORAGE_BUCKET=app # clubedge:only infra=docker
43
+ STORAGE_REGION=us-east-1 # clubedge:only infra=docker
44
+ STORAGE_ENDPOINT=http://localhost:9090 # clubedge:only infra=docker
45
+ STORAGE_ACCESS_KEY_ID=local # clubedge:only infra=docker
46
+ STORAGE_SECRET_ACCESS_KEY=local-secret # clubedge:only infra=docker
34
47
  STORAGE_PUBLIC_URL=
35
48
  # clubedge:end
36
49
 
37
50
  # clubedge:if storage=supabase
38
51
  # Supabase Storage bucket, accessed with the signed-in user's Supabase session
39
- SUPABASE_STORAGE_BUCKET=
52
+ SUPABASE_STORAGE_BUCKET= # clubedge:only infra!=supabase
53
+ SUPABASE_STORAGE_BUCKET=app # clubedge:only infra=supabase
40
54
  # clubedge:end
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/start",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -8,7 +8,8 @@
8
8
  "dev": "vite dev",
9
9
  "build": "vite build",
10
10
  "start": "node --env-file-if-exists=.env.local .output/server/index.mjs",
11
- "typecheck": "tsr generate && tsc --noEmit"
11
+ "typecheck": "tsr generate && tsc --noEmit",
12
+ "check:services": "tsx scripts/check-services.ts"
12
13
  },
13
14
  "dependencies": {
14
15
  "@clubedge/auth": "workspace:*",
@@ -38,6 +39,7 @@
38
39
  "@vitejs/plugin-react": "^6.1.2",
39
40
  "nitro": "3.0.260903-beta",
40
41
  "tailwindcss": "^4.1.13",
42
+ "tsx": "^4.20.5",
41
43
  "vite": "^8.3.4"
42
44
  }
43
45
  }
@@ -0,0 +1,106 @@
1
+ // Checks every service this project uses with a real round trip: `pnpm check:services`.
2
+ // It reads the app's .env.local like the app does, so it confirms the app's own settings.
3
+ import { existsSync } from "node:fs";
4
+ import { createDb } from "@clubedge/db";
5
+ // clubedge:if cache=redis
6
+ import { createRedisCache } from "@clubedge/cache-redis";
7
+ // clubedge:end
8
+ // clubedge:if storage=s3
9
+ import { createS3Storage } from "@clubedge/storage-s3";
10
+ // clubedge:end
11
+
12
+ type Check = { name: string; skip?: string; run: () => Promise<string> };
13
+
14
+ /** Services can take a few seconds to accept connections after `pnpm infra:up`. */
15
+ async function withRetry(run: () => Promise<string>, attempts = 15): Promise<string> {
16
+ for (let attempt = 1; ; attempt++) {
17
+ try {
18
+ return await run();
19
+ } catch (error) {
20
+ if (attempt >= attempts) throw error;
21
+ await new Promise((resolve) => setTimeout(resolve, 2_000));
22
+ }
23
+ }
24
+ }
25
+
26
+ async function main() {
27
+ if (existsSync(".env.local")) process.loadEnvFile(".env.local");
28
+ const env = process.env;
29
+
30
+ const checks: Check[] = [
31
+ {
32
+ name: "PostgreSQL",
33
+ skip: env.DATABASE_URL ? undefined : "DATABASE_URL is not set",
34
+ async run() {
35
+ const db = createDb({ url: env.DATABASE_URL! });
36
+ try {
37
+ const [row] = await db.$client`select current_database() as name`;
38
+ // clubedge:if auth=better-auth
39
+ // Better Auth's tables exist once `pnpm db:migrate` has run.
40
+ await db.$client`select 1 from auth_user limit 1`;
41
+ // clubedge:end
42
+ return `connected to database "${row?.name}"`;
43
+ } finally {
44
+ await db.$client.end();
45
+ }
46
+ },
47
+ },
48
+ // clubedge:if cache=redis
49
+ {
50
+ name: "Redis",
51
+ skip: env.REDIS_URL ? undefined : "REDIS_URL is not set; the app uses process memory",
52
+ async run() {
53
+ const cache = createRedisCache(env.REDIS_URL!);
54
+ const value = { checkedAt: Date.now() };
55
+ await cache.set("check-services", value, 30);
56
+ const read = await cache.get<typeof value>("check-services");
57
+ if (read?.checkedAt !== value.checkedAt) throw new Error("read back a different value");
58
+ return "wrote and read a cached value";
59
+ },
60
+ },
61
+ // clubedge:end
62
+ // clubedge:if storage=s3
63
+ {
64
+ name: "File storage",
65
+ skip: env.STORAGE_BUCKET ? undefined : "STORAGE_BUCKET is not set",
66
+ async run() {
67
+ const storage = createS3Storage({
68
+ bucket: env.STORAGE_BUCKET,
69
+ region: env.STORAGE_REGION || "auto",
70
+ endpoint: env.STORAGE_ENDPOINT || undefined,
71
+ accessKeyId: env.STORAGE_ACCESS_KEY_ID || undefined,
72
+ secretAccessKey: env.STORAGE_SECRET_ACCESS_KEY || undefined,
73
+ });
74
+ const key = `check-services/${Date.now()}.txt`;
75
+ await storage.upload({
76
+ key,
77
+ body: new TextEncoder().encode("ok"),
78
+ contentType: "text/plain",
79
+ });
80
+ const text = await new Response(await storage.download(key)).text();
81
+ await storage.delete(key);
82
+ if (text !== "ok") throw new Error(`read back "${text}"`);
83
+ return `uploaded, read back, and deleted an object in "${env.STORAGE_BUCKET}"`;
84
+ },
85
+ },
86
+ // clubedge:end
87
+ ];
88
+
89
+ let failed = false;
90
+ for (const check of checks) {
91
+ if (check.skip) {
92
+ console.log(`- ${check.name}: skipped (${check.skip})`);
93
+ continue;
94
+ }
95
+ try {
96
+ console.log(`✓ ${check.name}: ${await withRetry(check.run)}`);
97
+ } catch (error) {
98
+ failed = true;
99
+ console.log(`✗ ${check.name}: ${error instanceof Error ? error.message : String(error)}`);
100
+ }
101
+ }
102
+ // Redis clients keep the process alive, so exit explicitly.
103
+ process.exit(failed ? 1 : 0);
104
+ }
105
+
106
+ void main();
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@clubedge/web",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "scripts": {
7
7
  "dev": "next dev",
8
8
  "build": "next build",
9
9
  "start": "node scripts/start-standalone.mjs",
10
- "typecheck": "tsc --noEmit"
10
+ "typecheck": "tsc --noEmit",
11
+ "check:services": "tsx scripts/check-services.ts"
11
12
  },
12
13
  "dependencies": {
13
14
  "@clubedge/auth": "workspace:*",
@@ -33,6 +34,7 @@
33
34
  "@tailwindcss/postcss": "^4.1.13",
34
35
  "@types/react": "^19.1.13",
35
36
  "@types/react-dom": "^19.1.9",
36
- "tailwindcss": "^4.1.13"
37
+ "tailwindcss": "^4.1.13",
38
+ "tsx": "^4.20.5"
37
39
  }
38
40
  }
@@ -0,0 +1,106 @@
1
+ // Checks every service this project uses with a real round trip: `pnpm check:services`.
2
+ // It reads the app's .env.local like the app does, so it confirms the app's own settings.
3
+ import { existsSync } from "node:fs";
4
+ import { createDb } from "@clubedge/db";
5
+ // clubedge:if cache=redis
6
+ import { createRedisCache } from "@clubedge/cache-redis";
7
+ // clubedge:end
8
+ // clubedge:if storage=s3
9
+ import { createS3Storage } from "@clubedge/storage-s3";
10
+ // clubedge:end
11
+
12
+ type Check = { name: string; skip?: string; run: () => Promise<string> };
13
+
14
+ /** Services can take a few seconds to accept connections after `pnpm infra:up`. */
15
+ async function withRetry(run: () => Promise<string>, attempts = 15): Promise<string> {
16
+ for (let attempt = 1; ; attempt++) {
17
+ try {
18
+ return await run();
19
+ } catch (error) {
20
+ if (attempt >= attempts) throw error;
21
+ await new Promise((resolve) => setTimeout(resolve, 2_000));
22
+ }
23
+ }
24
+ }
25
+
26
+ async function main() {
27
+ if (existsSync(".env.local")) process.loadEnvFile(".env.local");
28
+ const env = process.env;
29
+
30
+ const checks: Check[] = [
31
+ {
32
+ name: "PostgreSQL",
33
+ skip: env.DATABASE_URL ? undefined : "DATABASE_URL is not set",
34
+ async run() {
35
+ const db = createDb({ url: env.DATABASE_URL! });
36
+ try {
37
+ const [row] = await db.$client`select current_database() as name`;
38
+ // clubedge:if auth=better-auth
39
+ // Better Auth's tables exist once `pnpm db:migrate` has run.
40
+ await db.$client`select 1 from auth_user limit 1`;
41
+ // clubedge:end
42
+ return `connected to database "${row?.name}"`;
43
+ } finally {
44
+ await db.$client.end();
45
+ }
46
+ },
47
+ },
48
+ // clubedge:if cache=redis
49
+ {
50
+ name: "Redis",
51
+ skip: env.REDIS_URL ? undefined : "REDIS_URL is not set; the app uses process memory",
52
+ async run() {
53
+ const cache = createRedisCache(env.REDIS_URL!);
54
+ const value = { checkedAt: Date.now() };
55
+ await cache.set("check-services", value, 30);
56
+ const read = await cache.get<typeof value>("check-services");
57
+ if (read?.checkedAt !== value.checkedAt) throw new Error("read back a different value");
58
+ return "wrote and read a cached value";
59
+ },
60
+ },
61
+ // clubedge:end
62
+ // clubedge:if storage=s3
63
+ {
64
+ name: "File storage",
65
+ skip: env.STORAGE_BUCKET ? undefined : "STORAGE_BUCKET is not set",
66
+ async run() {
67
+ const storage = createS3Storage({
68
+ bucket: env.STORAGE_BUCKET,
69
+ region: env.STORAGE_REGION || "auto",
70
+ endpoint: env.STORAGE_ENDPOINT || undefined,
71
+ accessKeyId: env.STORAGE_ACCESS_KEY_ID || undefined,
72
+ secretAccessKey: env.STORAGE_SECRET_ACCESS_KEY || undefined,
73
+ });
74
+ const key = `check-services/${Date.now()}.txt`;
75
+ await storage.upload({
76
+ key,
77
+ body: new TextEncoder().encode("ok"),
78
+ contentType: "text/plain",
79
+ });
80
+ const text = await new Response(await storage.download(key)).text();
81
+ await storage.delete(key);
82
+ if (text !== "ok") throw new Error(`read back "${text}"`);
83
+ return `uploaded, read back, and deleted an object in "${env.STORAGE_BUCKET}"`;
84
+ },
85
+ },
86
+ // clubedge:end
87
+ ];
88
+
89
+ let failed = false;
90
+ for (const check of checks) {
91
+ if (check.skip) {
92
+ console.log(`- ${check.name}: skipped (${check.skip})`);
93
+ continue;
94
+ }
95
+ try {
96
+ console.log(`✓ ${check.name}: ${await withRetry(check.run)}`);
97
+ } catch (error) {
98
+ failed = true;
99
+ console.log(`✗ ${check.name}: ${error instanceof Error ? error.message : String(error)}`);
100
+ }
101
+ }
102
+ // Redis clients keep the process alive, so exit explicitly.
103
+ process.exit(failed ? 1 : 0);
104
+ }
105
+
106
+ void main();
@@ -160,6 +160,35 @@
160
160
  }
161
161
  }
162
162
  }
163
+ },
164
+ "infra": {
165
+ "name": "Local infrastructure",
166
+ "default": "docker",
167
+ "options": {
168
+ "docker": {
169
+ "name": "Docker Compose",
170
+ "files": ["docker-compose.dev.yml"],
171
+ "scripts": {
172
+ "infra:up": "docker compose -f docker-compose.dev.yml up -d --wait",
173
+ "infra:down": "docker compose -f docker-compose.dev.yml down",
174
+ "infra:reset": "docker compose -f docker-compose.dev.yml down --volumes",
175
+ "infra:logs": "docker compose -f docker-compose.dev.yml logs --follow"
176
+ }
177
+ },
178
+ "supabase": {
179
+ "name": "Supabase CLI",
180
+ "files": ["supabase"],
181
+ "scripts": {
182
+ "infra:up": "npx --yes supabase@2 start",
183
+ "infra:down": "npx --yes supabase@2 stop",
184
+ "infra:reset": "npx --yes supabase@2 stop --no-backup",
185
+ "infra:status": "npx --yes supabase@2 status"
186
+ }
187
+ },
188
+ "none": {
189
+ "name": "None (bring your own services)"
190
+ }
191
+ }
163
192
  }
164
193
  },
165
194
  "conditional": [
@@ -169,12 +198,16 @@
169
198
  "README.md",
170
199
  "SETUP.md",
171
200
  "apps/start/.env.example",
201
+ "apps/start/scripts/check-services.ts",
172
202
  "apps/start/src/env/auth.ts",
173
203
  "apps/start/src/routes/dashboard.tsx",
174
204
  "apps/start/src/routes/index.tsx",
205
+ "apps/web/scripts/check-services.ts",
175
206
  "apps/web/src/app/dashboard/page.tsx",
176
207
  "apps/web/src/app/page.tsx",
177
- "apps/web/src/env/auth.ts"
208
+ "apps/web/src/env/auth.ts",
209
+ "docker-compose.dev.yml",
210
+ "supabase/config.toml"
178
211
  ],
179
212
  "exclude": [
180
213
  ".github/workflows/starter.yml",
@@ -0,0 +1,42 @@
1
+ # Local services for development. `pnpm infra:up` starts them and waits until they are ready,
2
+ # `pnpm infra:down` stops them, and `pnpm infra:reset` also deletes their data. Ports listen on
3
+ # 127.0.0.1 only. This file is for development; production uses managed services.
4
+ services:
5
+ postgres:
6
+ image: postgres:17
7
+ environment:
8
+ POSTGRES_USER: postgres
9
+ POSTGRES_PASSWORD: postgres
10
+ POSTGRES_DB: app
11
+ ports:
12
+ - "127.0.0.1:5432:5432"
13
+ volumes:
14
+ - postgres-data:/var/lib/postgresql/data
15
+ healthcheck:
16
+ test: ["CMD-SHELL", "pg_isready -U postgres -d app"]
17
+ interval: 2s
18
+ timeout: 5s
19
+ retries: 30
20
+ # clubedge:if cache=redis
21
+ redis:
22
+ image: redis:8
23
+ ports:
24
+ - "127.0.0.1:6379:6379"
25
+ healthcheck:
26
+ test: ["CMD", "redis-cli", "ping"]
27
+ interval: 2s
28
+ timeout: 5s
29
+ retries: 30
30
+ # clubedge:end
31
+ # clubedge:if storage=s3
32
+ # S3-compatible storage for development, with the "app" bucket created at startup.
33
+ storage:
34
+ image: adobe/s3mock:5
35
+ environment:
36
+ COM_ADOBE_TESTING_S3MOCK_STORE_INITIAL_BUCKETS: app
37
+ ports:
38
+ - "127.0.0.1:9090:9090"
39
+ # clubedge:end
40
+
41
+ volumes:
42
+ postgres-data:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clubedge-starter-v1",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "packageManager": "pnpm@10.9.0",
@@ -27,7 +27,12 @@
27
27
  "test:e2e:ui": "playwright test --ui",
28
28
  "docker:dev": "docker compose up --build",
29
29
  "docker:build": "docker build -t clubedge-starter .",
30
- "docker:start": "docker run --rm -p 3000:3000 --env-file apps/web/.env.local clubedge-starter"
30
+ "docker:start": "docker run --rm -p 3000:3000 --env-file apps/web/.env.local clubedge-starter",
31
+ "check:services": "pnpm --filter @clubedge/web check:services",
32
+ "infra:up": "docker compose -f docker-compose.dev.yml up -d --wait",
33
+ "infra:down": "docker compose -f docker-compose.dev.yml down",
34
+ "infra:reset": "docker compose -f docker-compose.dev.yml down --volumes",
35
+ "infra:logs": "docker compose -f docker-compose.dev.yml logs --follow"
31
36
  },
32
37
  "devDependencies": {
33
38
  "@playwright/test": "^1.55.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/auth",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/auth-better-auth",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Better Auth adapter for @clubedge/auth, storing users and sessions in your own PostgreSQL.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/auth-supabase",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Supabase Auth adapter for @clubedge/auth.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/cache",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/cache-redis",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Redis rate limiter and cache for @clubedge/cache.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/core",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/db",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/storage",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/storage-s3",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "S3-compatible adapter for @clubedge/storage.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -37,6 +37,28 @@ describe("createS3Storage", () => {
37
37
  expect(S3Client).toHaveBeenCalledTimes(1);
38
38
  });
39
39
 
40
+ it("uses path-style addressing with a custom endpoint unless told otherwise", async () => {
41
+ send.mockResolvedValue({});
42
+ await createS3Storage({ bucket: "files", region: "auto" }).delete("a.txt");
43
+ await createS3Storage({
44
+ bucket: "files",
45
+ region: "auto",
46
+ endpoint: "http://localhost:9090",
47
+ }).delete("a.txt");
48
+ await createS3Storage({
49
+ bucket: "files",
50
+ region: "auto",
51
+ endpoint: "https://r2.example",
52
+ forcePathStyle: false,
53
+ }).delete("a.txt");
54
+
55
+ expect(vi.mocked(S3Client).mock.calls.map(([options]) => options?.forcePathStyle)).toEqual([
56
+ false,
57
+ true,
58
+ false,
59
+ ]);
60
+ });
61
+
40
62
  it("uploads to the configured bucket and builds the public URL", async () => {
41
63
  const storage = createS3Storage({
42
64
  bucket: "files",
@@ -13,6 +13,11 @@ export interface S3StorageConfig {
13
13
  bucket?: string;
14
14
  region: string;
15
15
  endpoint?: string;
16
+ /**
17
+ * Address buckets as https://endpoint/bucket rather than https://bucket.endpoint. Defaults to
18
+ * true with a custom endpoint, which local servers (s3mock), MinIO, and R2 all accept.
19
+ */
20
+ forcePathStyle?: boolean;
16
21
  accessKeyId?: string;
17
22
  secretAccessKey?: string;
18
23
  /** Public base URL for uploaded objects, such as a CDN or R2 public bucket. */
@@ -25,7 +30,7 @@ function bodyForS3(body: UploadInput["body"]) {
25
30
  return body;
26
31
  }
27
32
 
28
- /** Works with AWS S3 and S3-compatible services such as Cloudflare R2 and MinIO. */
33
+ /** Works with AWS S3 and S3-compatible services such as Cloudflare R2, MinIO, and s3mock. */
29
34
  export function createS3Storage(config: S3StorageConfig): StorageProvider {
30
35
  let s3Client: S3Client | undefined;
31
36
 
@@ -33,6 +38,7 @@ export function createS3Storage(config: S3StorageConfig): StorageProvider {
33
38
  s3Client ??= new S3Client({
34
39
  region: config.region,
35
40
  ...(config.endpoint ? { endpoint: config.endpoint } : {}),
41
+ forcePathStyle: config.forcePathStyle ?? Boolean(config.endpoint),
36
42
  credentials:
37
43
  config.accessKeyId && config.secretAccessKey
38
44
  ? { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/storage-supabase",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Supabase Storage adapter for @clubedge/storage.",
5
5
  "private": true,
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/ui",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -13,7 +13,9 @@ export default defineConfig({
13
13
  url: "http://127.0.0.1:3000",
14
14
  reuseExistingServer: !process.env.CI,
15
15
  env: {
16
- DATABASE_URL: "postgresql://postgres:postgres@127.0.0.1:5432/postgres",
16
+ // A placeholder unless set, so pages render without a database.
17
+ DATABASE_URL:
18
+ process.env.DATABASE_URL ?? "postgresql://postgres:postgres@127.0.0.1:5432/postgres",
17
19
  // Next.js reads NEXT_PUBLIC_APP_URL; TanStack Start reads APP_URL.
18
20
  NEXT_PUBLIC_APP_URL: "http://127.0.0.1:3000",
19
21
  APP_URL: "http://127.0.0.1:3000",
@@ -114,6 +114,9 @@ importers:
114
114
  tailwindcss:
115
115
  specifier: ^4.1.13
116
116
  version: 4.3.3
117
+ tsx:
118
+ specifier: ^4.20.5
119
+ version: 4.23.15
117
120
  vite:
118
121
  specifier: ^8.3.4
119
122
  version: 8.3.4(@types/node@22.20.5)(esbuild@0.25.12)(jiti@2.7.0)(tsx@4.23.15)
@@ -187,6 +190,9 @@ importers:
187
190
  tailwindcss:
188
191
  specifier: ^4.1.13
189
192
  version: 4.3.3
193
+ tsx:
194
+ specifier: ^4.20.5
195
+ version: 4.23.15
190
196
 
191
197
  packages/auth:
192
198
  dependencies:
@@ -0,0 +1,40 @@
1
+ # Local Supabase stack for development: `pnpm infra:up` runs `supabase start`, and
2
+ # `pnpm infra:status` prints the local URLs and keys. Docs: https://supabase.com/docs/guides/local-development
3
+ project_id = "clubedge-app"
4
+
5
+ [api]
6
+ port = 54321
7
+
8
+ [db]
9
+ port = 54322
10
+ major_version = 17
11
+
12
+ [studio]
13
+ port = 54323
14
+
15
+ # Captures sign-up and password emails locally; open it from `pnpm infra:status`.
16
+ [inbucket]
17
+ port = 54324
18
+
19
+ [auth]
20
+ site_url = "http://localhost:3000"
21
+ additional_redirect_urls = ["http://localhost:3000/auth/callback", "http://127.0.0.1:3000/auth/callback"]
22
+
23
+ [auth.email]
24
+ # Sign-up signs the user in directly during local development.
25
+ enable_confirmations = false
26
+
27
+ # clubedge:if storage=supabase
28
+ [storage.buckets.app]
29
+ public = false
30
+ # clubedge:end
31
+
32
+ # Not used by the app; disabled so the stack starts faster.
33
+ [realtime]
34
+ enabled = false
35
+
36
+ [edge_runtime]
37
+ enabled = false
38
+
39
+ [analytics]
40
+ enabled = false
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "repository": "Clubedge/clubedge-starter",
3
- "ref": "v0.5.0",
4
- "commit": "d9375c2c3b520323ff0255ef8ec65da19251c3c1",
3
+ "ref": "v0.6.0",
4
+ "commit": "bf5a5e801254816455cbb837902283b788ab7595",
5
5
  "files": [
6
6
  ".dockerignore",
7
7
  ".env.example",
@@ -32,6 +32,7 @@
32
32
  "apps/start/package.json",
33
33
  "apps/start/public/.well-known/logo.svg",
34
34
  "apps/start/public/favicon.ico",
35
+ "apps/start/scripts/check-services.ts",
35
36
  "apps/start/src/components/auth/auth-form-controls.tsx",
36
37
  "apps/start/src/components/branding/clubedge-mark.tsx",
37
38
  "apps/start/src/components/dashboard/account-button.tsx",
@@ -87,6 +88,7 @@
87
88
  "apps/web/postcss.config.mjs",
88
89
  "apps/web/public/.gitkeep",
89
90
  "apps/web/public/.well-known/logo.svg",
91
+ "apps/web/scripts/check-services.ts",
90
92
  "apps/web/scripts/start-standalone.mjs",
91
93
  "apps/web/src/app/actions/auth.ts",
92
94
  "apps/web/src/app/api/health/route.ts",
@@ -127,6 +129,7 @@
127
129
  "apps/web/src/variants/proxy.better-auth.ts",
128
130
  "apps/web/tsconfig.json",
129
131
  "clubedge.template.json",
132
+ "docker-compose.dev.yml",
130
133
  "docker-compose.yml",
131
134
  "e2e/app.ts",
132
135
  "e2e/auth-flow.spec.ts",
@@ -219,6 +222,7 @@
219
222
  "playwright.config.ts",
220
223
  "pnpm-lock.yaml",
221
224
  "pnpm-workspace.yaml",
225
+ "supabase/config.toml",
222
226
  "tsconfig.base.json",
223
227
  "turbo.json",
224
228
  "vitest.config.ts"