@clubedge/create-clubedge-app 0.5.0 → 0.7.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 (49) hide show
  1. package/CHANGELOG.md +12 -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 +12 -3
  6. package/dist/core/lockfile-peers.js +174 -0
  7. package/dist/core/manifest.js +1 -0
  8. package/dist/core/plan.js +15 -1
  9. package/dist/core/source.js +12 -9
  10. package/dist/index.js +1 -1
  11. package/package.json +3 -3
  12. package/template/.env.example +28 -10
  13. package/template/README.md +45 -1
  14. package/template/SETUP.md +54 -1
  15. package/template/_gitignore +5 -0
  16. package/template/apps/start/.env.example +28 -10
  17. package/template/apps/start/package.json +4 -2
  18. package/template/apps/start/scripts/check-services.ts +128 -0
  19. package/template/apps/start/src/server/storage.ts +13 -0
  20. package/template/apps/web/package.json +5 -3
  21. package/template/apps/web/scripts/check-services.ts +128 -0
  22. package/template/apps/web/src/server/storage.ts +13 -0
  23. package/template/clubedge.template.json +52 -1
  24. package/template/docker-compose.dev.yml +42 -0
  25. package/template/package.json +7 -2
  26. package/template/packages/auth/package.json +1 -1
  27. package/template/packages/auth-better-auth/package.json +1 -1
  28. package/template/packages/auth-supabase/package.json +1 -1
  29. package/template/packages/cache/package.json +1 -1
  30. package/template/packages/cache-redis/package.json +1 -1
  31. package/template/packages/core/package.json +1 -1
  32. package/template/packages/db/package.json +1 -1
  33. package/template/packages/local-db/package.json +24 -0
  34. package/template/packages/local-db/scripts/serve.ts +66 -0
  35. package/template/packages/local-db/src/server.test.ts +119 -0
  36. package/template/packages/local-db/src/server.ts +153 -0
  37. package/template/packages/local-db/tsconfig.json +4 -0
  38. package/template/packages/storage/package.json +3 -2
  39. package/template/packages/storage/src/file-system.test.ts +110 -0
  40. package/template/packages/storage/src/file-system.ts +124 -0
  41. package/template/packages/storage-s3/package.json +1 -1
  42. package/template/packages/storage-s3/src/index.test.ts +22 -0
  43. package/template/packages/storage-s3/src/index.ts +7 -1
  44. package/template/packages/storage-supabase/package.json +1 -1
  45. package/template/packages/ui/package.json +1 -1
  46. package/template/playwright.config.ts +3 -1
  47. package/template/pnpm-lock.yaml +37 -9
  48. package/template/supabase/config.toml +40 -0
  49. package/template.lock.json +13 -2
@@ -2,13 +2,21 @@
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
11
+ # Local PostgreSQL (PGlite) that pnpm dev starts; no Docker needed. # clubedge:only infra=local
12
+ DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5433/postgres # clubedge:only infra=local
7
13
 
8
14
  # clubedge:if auth=supabase
9
15
  # Supabase Auth (publishable key only; never use a service role key here).
10
16
  # All auth runs on the server, so these need no browser prefix.
11
- SUPABASE_URL=
17
+ SUPABASE_URL= # clubedge:only infra!=supabase
18
+ # Local Supabase Auth. pnpm infra:status prints the publishable key to copy below. # clubedge:only infra=supabase
19
+ SUPABASE_URL=http://127.0.0.1:54321 # clubedge:only infra=supabase
12
20
  SUPABASE_PUBLISHABLE_KEY=
13
21
  # clubedge:end
14
22
 
@@ -21,20 +29,30 @@ BETTER_AUTH_SECRET=
21
29
  # clubedge:if cache=redis
22
30
  # Optional cache and rate limits (Redis protocol; rediss:// enables TLS). Without it, both use
23
31
  # process memory. Upstash example: rediss://default:YOUR_PASSWORD@your-database.upstash.io:6379
24
- REDIS_URL=
32
+ REDIS_URL= # clubedge:only infra!=docker
33
+ REDIS_URL=redis://localhost:6379 # clubedge:only infra=docker
25
34
  # clubedge:end
26
35
 
27
36
  # clubedge:if storage=s3
28
37
  # 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=
38
+ STORAGE_BUCKET= # clubedge:only infra!=docker
39
+ STORAGE_REGION=auto # clubedge:only infra!=docker
40
+ STORAGE_ENDPOINT= # clubedge:only infra!=docker
41
+ STORAGE_ACCESS_KEY_ID= # clubedge:only infra!=docker
42
+ STORAGE_SECRET_ACCESS_KEY= # clubedge:only infra!=docker
43
+ # Local S3-compatible storage from docker-compose.dev.yml; it accepts any credentials. # clubedge:only infra=docker
44
+ STORAGE_BUCKET=app # clubedge:only infra=docker
45
+ STORAGE_REGION=us-east-1 # clubedge:only infra=docker
46
+ STORAGE_ENDPOINT=http://localhost:9090 # clubedge:only infra=docker
47
+ STORAGE_ACCESS_KEY_ID=local # clubedge:only infra=docker
48
+ STORAGE_SECRET_ACCESS_KEY=local-secret # clubedge:only infra=docker
34
49
  STORAGE_PUBLIC_URL=
50
+ # Without an S3 service, files are stored in this folder (relative to the app). # clubedge:only infra=local
51
+ STORAGE_DIRECTORY=../../.data/storage # clubedge:only infra=local
35
52
  # clubedge:end
36
53
 
37
54
  # clubedge:if storage=supabase
38
55
  # Supabase Storage bucket, accessed with the signed-in user's Supabase session
39
- SUPABASE_STORAGE_BUCKET=
56
+ SUPABASE_STORAGE_BUCKET= # clubedge:only infra!=supabase
57
+ SUPABASE_STORAGE_BUCKET=app # clubedge:only infra=supabase
40
58
  # clubedge:end
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clubedge/start",
3
- "version": "0.5.0",
3
+ "version": "0.7.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,128 @@
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
+ // clubedge:if storage=s3 && infra=local
12
+ import { createFileSystemStorage } from "@clubedge/storage/file-system";
13
+ // clubedge:end
14
+
15
+ type Check = { name: string; skip?: string; run: () => Promise<string> };
16
+
17
+ /** Services can take a few seconds to accept connections after `pnpm infra:up`. */
18
+ async function withRetry(run: () => Promise<string>, attempts = 15): Promise<string> {
19
+ for (let attempt = 1; ; attempt++) {
20
+ try {
21
+ return await run();
22
+ } catch (error) {
23
+ if (attempt >= attempts) throw error;
24
+ await new Promise((resolve) => setTimeout(resolve, 2_000));
25
+ }
26
+ }
27
+ }
28
+
29
+ async function main() {
30
+ if (existsSync(".env.local")) process.loadEnvFile(".env.local");
31
+ const env = process.env;
32
+
33
+ const checks: Check[] = [
34
+ {
35
+ name: "PostgreSQL",
36
+ skip: env.DATABASE_URL ? undefined : "DATABASE_URL is not set",
37
+ async run() {
38
+ const db = createDb({ url: env.DATABASE_URL! });
39
+ try {
40
+ const [row] = await db.$client`select current_database() as name`;
41
+ // clubedge:if auth=better-auth
42
+ // Better Auth's tables exist once `pnpm db:migrate` has run.
43
+ await db.$client`select 1 from auth_user limit 1`;
44
+ // clubedge:end
45
+ return `connected to database "${row?.name}"`;
46
+ } finally {
47
+ await db.$client.end();
48
+ }
49
+ },
50
+ },
51
+ // clubedge:if cache=redis
52
+ {
53
+ name: "Redis",
54
+ skip: env.REDIS_URL ? undefined : "REDIS_URL is not set; the app uses process memory",
55
+ async run() {
56
+ const cache = createRedisCache(env.REDIS_URL!);
57
+ const value = { checkedAt: Date.now() };
58
+ await cache.set("check-services", value, 30);
59
+ const read = await cache.get<typeof value>("check-services");
60
+ if (read?.checkedAt !== value.checkedAt) throw new Error("read back a different value");
61
+ return "wrote and read a cached value";
62
+ },
63
+ },
64
+ // clubedge:end
65
+ // clubedge:if storage=s3 && infra=local
66
+ {
67
+ name: "Local folder storage",
68
+ skip: env.STORAGE_DIRECTORY ? undefined : "STORAGE_DIRECTORY is not set",
69
+ async run() {
70
+ const storage = createFileSystemStorage({ directory: env.STORAGE_DIRECTORY! });
71
+ const key = `check-services/${Date.now()}.txt`;
72
+ await storage.upload({
73
+ key,
74
+ body: new TextEncoder().encode("ok"),
75
+ contentType: "text/plain",
76
+ });
77
+ const text = await new Response(await storage.download(key)).text();
78
+ await storage.delete(key);
79
+ if (text !== "ok") throw new Error(`read back "${text}"`);
80
+ return `wrote, read back, and deleted a file in ${env.STORAGE_DIRECTORY}`;
81
+ },
82
+ },
83
+ // clubedge:end
84
+ // clubedge:if storage=s3
85
+ {
86
+ name: "File storage",
87
+ skip: env.STORAGE_BUCKET ? undefined : "STORAGE_BUCKET is not set",
88
+ async run() {
89
+ const storage = createS3Storage({
90
+ bucket: env.STORAGE_BUCKET,
91
+ region: env.STORAGE_REGION || "auto",
92
+ endpoint: env.STORAGE_ENDPOINT || undefined,
93
+ accessKeyId: env.STORAGE_ACCESS_KEY_ID || undefined,
94
+ secretAccessKey: env.STORAGE_SECRET_ACCESS_KEY || undefined,
95
+ });
96
+ const key = `check-services/${Date.now()}.txt`;
97
+ await storage.upload({
98
+ key,
99
+ body: new TextEncoder().encode("ok"),
100
+ contentType: "text/plain",
101
+ });
102
+ const text = await new Response(await storage.download(key)).text();
103
+ await storage.delete(key);
104
+ if (text !== "ok") throw new Error(`read back "${text}"`);
105
+ return `uploaded, read back, and deleted an object in "${env.STORAGE_BUCKET}"`;
106
+ },
107
+ },
108
+ // clubedge:end
109
+ ];
110
+
111
+ let failed = false;
112
+ for (const check of checks) {
113
+ if (check.skip) {
114
+ console.log(`- ${check.name}: skipped (${check.skip})`);
115
+ continue;
116
+ }
117
+ try {
118
+ console.log(`✓ ${check.name}: ${await withRetry(check.run)}`);
119
+ } catch (error) {
120
+ failed = true;
121
+ console.log(`✗ ${check.name}: ${error instanceof Error ? error.message : String(error)}`);
122
+ }
123
+ }
124
+ // Redis clients keep the process alive, so exit explicitly.
125
+ process.exit(failed ? 1 : 0);
126
+ }
127
+
128
+ void main();
@@ -2,6 +2,9 @@ import "@tanstack/react-start/server-only";
2
2
  import { z } from "zod";
3
3
  import type { StorageProvider } from "@clubedge/storage";
4
4
  import { createS3Storage } from "@clubedge/storage-s3";
5
+ // clubedge:if infra=local
6
+ import { createFileSystemStorage } from "@clubedge/storage/file-system";
7
+ // clubedge:end
5
8
  import { optional, parseEnv } from "@/env/server";
6
9
 
7
10
  export type { SignedUrlOptions, StorageProvider, UploadInput } from "@clubedge/storage";
@@ -14,6 +17,9 @@ const storageEnvSchema = z.object({
14
17
  STORAGE_ACCESS_KEY_ID: optional(z.string()),
15
18
  STORAGE_SECRET_ACCESS_KEY: optional(z.string()),
16
19
  STORAGE_PUBLIC_URL: optional(z.url()),
20
+ // clubedge:if infra=local
21
+ STORAGE_DIRECTORY: optional(z.string()),
22
+ // clubedge:end
17
23
  });
18
24
 
19
25
  let provider: StorageProvider | undefined;
@@ -21,6 +27,13 @@ let provider: StorageProvider | undefined;
21
27
  function resolveProvider(): StorageProvider {
22
28
  if (provider) return provider;
23
29
  const env = parseEnv(storageEnvSchema, "Storage");
30
+ // clubedge:if infra=local
31
+ // In development without an object storage service, a local folder stands in for S3.
32
+ if (env.STORAGE_DIRECTORY) {
33
+ provider = createFileSystemStorage({ directory: env.STORAGE_DIRECTORY });
34
+ return provider;
35
+ }
36
+ // clubedge:end
24
37
  provider = createS3Storage({
25
38
  bucket: env.STORAGE_BUCKET || undefined,
26
39
  region: env.STORAGE_REGION,
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@clubedge/web",
3
- "version": "0.5.0",
3
+ "version": "0.7.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,128 @@
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
+ // clubedge:if storage=s3 && infra=local
12
+ import { createFileSystemStorage } from "@clubedge/storage/file-system";
13
+ // clubedge:end
14
+
15
+ type Check = { name: string; skip?: string; run: () => Promise<string> };
16
+
17
+ /** Services can take a few seconds to accept connections after `pnpm infra:up`. */
18
+ async function withRetry(run: () => Promise<string>, attempts = 15): Promise<string> {
19
+ for (let attempt = 1; ; attempt++) {
20
+ try {
21
+ return await run();
22
+ } catch (error) {
23
+ if (attempt >= attempts) throw error;
24
+ await new Promise((resolve) => setTimeout(resolve, 2_000));
25
+ }
26
+ }
27
+ }
28
+
29
+ async function main() {
30
+ if (existsSync(".env.local")) process.loadEnvFile(".env.local");
31
+ const env = process.env;
32
+
33
+ const checks: Check[] = [
34
+ {
35
+ name: "PostgreSQL",
36
+ skip: env.DATABASE_URL ? undefined : "DATABASE_URL is not set",
37
+ async run() {
38
+ const db = createDb({ url: env.DATABASE_URL! });
39
+ try {
40
+ const [row] = await db.$client`select current_database() as name`;
41
+ // clubedge:if auth=better-auth
42
+ // Better Auth's tables exist once `pnpm db:migrate` has run.
43
+ await db.$client`select 1 from auth_user limit 1`;
44
+ // clubedge:end
45
+ return `connected to database "${row?.name}"`;
46
+ } finally {
47
+ await db.$client.end();
48
+ }
49
+ },
50
+ },
51
+ // clubedge:if cache=redis
52
+ {
53
+ name: "Redis",
54
+ skip: env.REDIS_URL ? undefined : "REDIS_URL is not set; the app uses process memory",
55
+ async run() {
56
+ const cache = createRedisCache(env.REDIS_URL!);
57
+ const value = { checkedAt: Date.now() };
58
+ await cache.set("check-services", value, 30);
59
+ const read = await cache.get<typeof value>("check-services");
60
+ if (read?.checkedAt !== value.checkedAt) throw new Error("read back a different value");
61
+ return "wrote and read a cached value";
62
+ },
63
+ },
64
+ // clubedge:end
65
+ // clubedge:if storage=s3 && infra=local
66
+ {
67
+ name: "Local folder storage",
68
+ skip: env.STORAGE_DIRECTORY ? undefined : "STORAGE_DIRECTORY is not set",
69
+ async run() {
70
+ const storage = createFileSystemStorage({ directory: env.STORAGE_DIRECTORY! });
71
+ const key = `check-services/${Date.now()}.txt`;
72
+ await storage.upload({
73
+ key,
74
+ body: new TextEncoder().encode("ok"),
75
+ contentType: "text/plain",
76
+ });
77
+ const text = await new Response(await storage.download(key)).text();
78
+ await storage.delete(key);
79
+ if (text !== "ok") throw new Error(`read back "${text}"`);
80
+ return `wrote, read back, and deleted a file in ${env.STORAGE_DIRECTORY}`;
81
+ },
82
+ },
83
+ // clubedge:end
84
+ // clubedge:if storage=s3
85
+ {
86
+ name: "File storage",
87
+ skip: env.STORAGE_BUCKET ? undefined : "STORAGE_BUCKET is not set",
88
+ async run() {
89
+ const storage = createS3Storage({
90
+ bucket: env.STORAGE_BUCKET,
91
+ region: env.STORAGE_REGION || "auto",
92
+ endpoint: env.STORAGE_ENDPOINT || undefined,
93
+ accessKeyId: env.STORAGE_ACCESS_KEY_ID || undefined,
94
+ secretAccessKey: env.STORAGE_SECRET_ACCESS_KEY || undefined,
95
+ });
96
+ const key = `check-services/${Date.now()}.txt`;
97
+ await storage.upload({
98
+ key,
99
+ body: new TextEncoder().encode("ok"),
100
+ contentType: "text/plain",
101
+ });
102
+ const text = await new Response(await storage.download(key)).text();
103
+ await storage.delete(key);
104
+ if (text !== "ok") throw new Error(`read back "${text}"`);
105
+ return `uploaded, read back, and deleted an object in "${env.STORAGE_BUCKET}"`;
106
+ },
107
+ },
108
+ // clubedge:end
109
+ ];
110
+
111
+ let failed = false;
112
+ for (const check of checks) {
113
+ if (check.skip) {
114
+ console.log(`- ${check.name}: skipped (${check.skip})`);
115
+ continue;
116
+ }
117
+ try {
118
+ console.log(`✓ ${check.name}: ${await withRetry(check.run)}`);
119
+ } catch (error) {
120
+ failed = true;
121
+ console.log(`✗ ${check.name}: ${error instanceof Error ? error.message : String(error)}`);
122
+ }
123
+ }
124
+ // Redis clients keep the process alive, so exit explicitly.
125
+ process.exit(failed ? 1 : 0);
126
+ }
127
+
128
+ void main();
@@ -2,6 +2,9 @@ import "server-only";
2
2
  import { z } from "zod";
3
3
  import type { StorageProvider } from "@clubedge/storage";
4
4
  import { createS3Storage } from "@clubedge/storage-s3";
5
+ // clubedge:if infra=local
6
+ import { createFileSystemStorage } from "@clubedge/storage/file-system";
7
+ // clubedge:end
5
8
  import { optional, parseEnv } from "@/env/server";
6
9
 
7
10
  export type { SignedUrlOptions, StorageProvider, UploadInput } from "@clubedge/storage";
@@ -14,6 +17,9 @@ const storageEnvSchema = z.object({
14
17
  STORAGE_ACCESS_KEY_ID: optional(z.string()),
15
18
  STORAGE_SECRET_ACCESS_KEY: optional(z.string()),
16
19
  STORAGE_PUBLIC_URL: optional(z.url()),
20
+ // clubedge:if infra=local
21
+ STORAGE_DIRECTORY: optional(z.string()),
22
+ // clubedge:end
17
23
  });
18
24
 
19
25
  let provider: StorageProvider | undefined;
@@ -21,6 +27,13 @@ let provider: StorageProvider | undefined;
21
27
  function resolveProvider(): StorageProvider {
22
28
  if (provider) return provider;
23
29
  const env = parseEnv(storageEnvSchema, "Storage");
30
+ // clubedge:if infra=local
31
+ // In development without an object storage service, a local folder stands in for S3.
32
+ if (env.STORAGE_DIRECTORY) {
33
+ provider = createFileSystemStorage({ directory: env.STORAGE_DIRECTORY });
34
+ return provider;
35
+ }
36
+ // clubedge:end
24
37
  provider = createS3Storage({
25
38
  bucket: env.STORAGE_BUCKET || undefined,
26
39
  region: env.STORAGE_REGION,
@@ -160,6 +160,51 @@
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
+ "dev": "turbo run dev --filter=@clubedge/web",
173
+ "infra:up": "docker compose -f docker-compose.dev.yml up -d --wait",
174
+ "infra:down": "docker compose -f docker-compose.dev.yml down",
175
+ "infra:reset": "docker compose -f docker-compose.dev.yml down --volumes",
176
+ "infra:logs": "docker compose -f docker-compose.dev.yml logs --follow"
177
+ }
178
+ },
179
+ "supabase": {
180
+ "name": "Supabase CLI",
181
+ "files": ["supabase"],
182
+ "scripts": {
183
+ "dev": "turbo run dev --filter=@clubedge/web",
184
+ "infra:up": "npx --yes supabase@2 start",
185
+ "infra:down": "npx --yes supabase@2 stop",
186
+ "infra:reset": "npx --yes supabase@2 stop --no-backup",
187
+ "infra:status": "npx --yes supabase@2 status"
188
+ }
189
+ },
190
+ "local": {
191
+ "name": "No Docker (PGlite and a local folder)",
192
+ "packages": {
193
+ "@clubedge/local-db": "packages/local-db"
194
+ },
195
+ "scripts": {
196
+ "dev": "turbo run dev --filter=@clubedge/web --filter=@clubedge/local-db",
197
+ "infra:up": "pnpm --filter @clubedge/local-db dev",
198
+ "infra:reset": "node -e \"require('node:fs').rmSync('.data', { recursive: true, force: true })\""
199
+ }
200
+ },
201
+ "none": {
202
+ "name": "None (bring your own services)",
203
+ "scripts": {
204
+ "dev": "turbo run dev --filter=@clubedge/web"
205
+ }
206
+ }
207
+ }
163
208
  }
164
209
  },
165
210
  "conditional": [
@@ -169,12 +214,18 @@
169
214
  "README.md",
170
215
  "SETUP.md",
171
216
  "apps/start/.env.example",
217
+ "apps/start/scripts/check-services.ts",
172
218
  "apps/start/src/env/auth.ts",
173
219
  "apps/start/src/routes/dashboard.tsx",
174
220
  "apps/start/src/routes/index.tsx",
221
+ "apps/start/src/server/storage.ts",
222
+ "apps/web/scripts/check-services.ts",
175
223
  "apps/web/src/app/dashboard/page.tsx",
176
224
  "apps/web/src/app/page.tsx",
177
- "apps/web/src/env/auth.ts"
225
+ "apps/web/src/env/auth.ts",
226
+ "apps/web/src/server/storage.ts",
227
+ "docker-compose.dev.yml",
228
+ "supabase/config.toml"
178
229
  ],
179
230
  "exclude": [
180
231
  ".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.7.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.7.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.7.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.7.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.7.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.7.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.7.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.7.0",
4
4
  "private": true,
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@clubedge/local-db",
3
+ "version": "0.7.0",
4
+ "description": "PostgreSQL for development without Docker: PGlite served over the PostgreSQL protocol.",
5
+ "private": true,
6
+ "license": "Apache-2.0",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": "./src/server.ts"
11
+ },
12
+ "scripts": {
13
+ "dev": "tsx scripts/serve.ts",
14
+ "typecheck": "tsc --noEmit"
15
+ },
16
+ "dependencies": {
17
+ "@electric-sql/pglite": "0.5.8"
18
+ },
19
+ "devDependencies": {
20
+ "postgres": "^3.4.7",
21
+ "tsx": "^4.20.5",
22
+ "typescript": "^5.9.2"
23
+ }
24
+ }