create-apollo-monorepo 0.1.0 → 0.3.1
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/README.md +35 -1
- package/index.mjs +322 -32
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -33,6 +33,34 @@ thamc-new/
|
|
|
33
33
|
└── .gitignore
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
## Routing modes
|
|
37
|
+
|
|
38
|
+
### Single-origin (default)
|
|
39
|
+
|
|
40
|
+
Both apps share one public origin (the frontend). The frontend's
|
|
41
|
+
`next.config.ts` rewrites these paths to the backend so `/_next/*` doesn't
|
|
42
|
+
collide:
|
|
43
|
+
|
|
44
|
+
| Browser path | Goes to |
|
|
45
|
+
| ------------------------------------ | -------------------- |
|
|
46
|
+
| `/`, your custom routes | `apps/frontend` |
|
|
47
|
+
| `/admin/*` | `apps/backend` |
|
|
48
|
+
| `/api/auth/*`, `/api/v1/*`, `/api/admin/*`, `/api/email/*`, `/api/cron`, `/api/health`, `/api/mcp`, `/api/editing-presence/*` | `apps/backend` |
|
|
49
|
+
| `/uploads/*` | `apps/backend` (media) |
|
|
50
|
+
| `/cms-assets/*` | `apps/backend` chunks (via `APOLLO_ASSET_PREFIX`) |
|
|
51
|
+
| `/monitoring` | `apps/backend` (Sentry tunnel, no-op without DSN) |
|
|
52
|
+
|
|
53
|
+
Apollo CMS picks up `APOLLO_ASSET_PREFIX=/cms-assets` and serves its built JS
|
|
54
|
+
under that namespace, sidestepping the `/_next/*` collision. The frontend
|
|
55
|
+
**must not** define routes at `/admin`, `/api/auth`, `/api/v1`, etc.
|
|
56
|
+
|
|
57
|
+
### Separate origins (fallback)
|
|
58
|
+
|
|
59
|
+
Pass `--asset-prefix none` (or `off`/`false`/`disabled`) to skip the rewrite
|
|
60
|
+
wiring. The backend runs at `http://localhost:3000` and the frontend at
|
|
61
|
+
`http://localhost:3001`. Useful when you'd rather deploy them on separate
|
|
62
|
+
subdomains (e.g. `cms.example.com` + `example.com`).
|
|
63
|
+
|
|
36
64
|
## Flags
|
|
37
65
|
|
|
38
66
|
| Flag | Default | Description |
|
|
@@ -41,8 +69,9 @@ thamc-new/
|
|
|
41
69
|
| `--backend-url <url>` | `https://github.com/5Lab-Group-Co-Ltd/apollo-cms.git` | Submodule git URL |
|
|
42
70
|
| `--backend-branch <name>` | `main` | Submodule branch to track |
|
|
43
71
|
| `-d, --db <url>` | _(prompted)_ | `DATABASE_URL` for backend |
|
|
44
|
-
| `-u, --url <url>` | `
|
|
72
|
+
| `-u, --url <url>` | `:3001` single-origin / `:3000` separate | `NEXT_PUBLIC_SITE_URL` |
|
|
45
73
|
| `-l, --locale <code>` | `en` | `NEXT_PUBLIC_DEFAULT_LOCALE` |
|
|
74
|
+
| `--asset-prefix <path>` | `/cms-assets` | Single-origin asset namespace; `none` to disable |
|
|
46
75
|
| `--skip-install` | off | Don't run `pnpm install` |
|
|
47
76
|
| `--skip-submodule` | off | Don't add the git submodule |
|
|
48
77
|
| `-h, --help` | — | Show help |
|
|
@@ -55,6 +84,11 @@ pnpm backend:setup # push schema + seed apollo-cms
|
|
|
55
84
|
pnpm dev # frontend :3001 + backend :3000 in parallel
|
|
56
85
|
```
|
|
57
86
|
|
|
87
|
+
In single-origin mode open `http://localhost:3001/admin` (the frontend port);
|
|
88
|
+
the rewrite proxies to the backend and Better Auth's cookies/origins line up
|
|
89
|
+
because `NEXT_PUBLIC_SITE_URL` and the backend's `trustedProxyHeaders` are wired
|
|
90
|
+
to the public origin.
|
|
91
|
+
|
|
58
92
|
## Updating the backend
|
|
59
93
|
|
|
60
94
|
```bash
|
package/index.mjs
CHANGED
|
@@ -54,11 +54,21 @@ ${COLORS.bold}Flags:${COLORS.reset}
|
|
|
54
54
|
--backend-url <url> Backend git URL (default: ${BACKEND_REPO_URL})
|
|
55
55
|
--backend-branch <name> Backend branch to track (default: ${BACKEND_BRANCH})
|
|
56
56
|
-d, --db <url> DATABASE_URL for backend
|
|
57
|
-
-u, --url <url> NEXT_PUBLIC_SITE_URL (default: http://localhost:3000)
|
|
57
|
+
-u, --url <url> NEXT_PUBLIC_SITE_URL (default: http://localhost:3001 single-origin / 3000 separate)
|
|
58
58
|
-l, --locale <code> NEXT_PUBLIC_DEFAULT_LOCALE (default: en)
|
|
59
|
+
--asset-prefix <path> Single-origin asset namespace (default: /cms-assets, or "none" to disable)
|
|
59
60
|
--skip-install Skip dependency installation
|
|
60
61
|
--skip-submodule Skip git submodule add (you'll add it later)
|
|
61
62
|
-h, --help Show this help message
|
|
63
|
+
|
|
64
|
+
${COLORS.bold}Single-origin model:${COLORS.reset}
|
|
65
|
+
The frontend is the public entry point. It rewrites these paths to the
|
|
66
|
+
backend so both apps live under one domain without /_next/* collisions:
|
|
67
|
+
/admin/* → backend
|
|
68
|
+
/api/* → backend (REST API + auth)
|
|
69
|
+
/uploads/* → backend (media)
|
|
70
|
+
<asset-prefix>/* → backend (Next.js chunks via APOLLO_ASSET_PREFIX)
|
|
71
|
+
Frontend MUST NOT define routes at /admin or /api/auth or /api/v1.
|
|
62
72
|
`;
|
|
63
73
|
|
|
64
74
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
@@ -112,6 +122,7 @@ function parseArgs(argv) {
|
|
|
112
122
|
db: null,
|
|
113
123
|
url: null,
|
|
114
124
|
locale: null,
|
|
125
|
+
assetPrefix: "/cms-assets",
|
|
115
126
|
skipInstall: false,
|
|
116
127
|
skipSubmodule: false,
|
|
117
128
|
help: false,
|
|
@@ -146,6 +157,9 @@ function parseArgs(argv) {
|
|
|
146
157
|
case "--locale":
|
|
147
158
|
flags.locale = args[++i];
|
|
148
159
|
break;
|
|
160
|
+
case "--asset-prefix":
|
|
161
|
+
flags.assetPrefix = args[++i];
|
|
162
|
+
break;
|
|
149
163
|
case "--skip-install":
|
|
150
164
|
flags.skipInstall = true;
|
|
151
165
|
break;
|
|
@@ -181,6 +195,23 @@ function isValidLocale(code) {
|
|
|
181
195
|
return /^[a-z]{2,5}$/i.test(code);
|
|
182
196
|
}
|
|
183
197
|
|
|
198
|
+
// Normalize an asset prefix:
|
|
199
|
+
// - empty string / "none" / "off" / "false" → "" (single-origin disabled, fallback to separate origins)
|
|
200
|
+
// - "/foo" → "/foo"
|
|
201
|
+
// - "foo" → "/foo"
|
|
202
|
+
// - "/foo/" → "/foo"
|
|
203
|
+
function normalizeAssetPrefix(value) {
|
|
204
|
+
if (!value) return "";
|
|
205
|
+
const trimmed = String(value).trim();
|
|
206
|
+
if (trimmed === "" || /^(none|off|false|disabled)$/i.test(trimmed)) return "";
|
|
207
|
+
let p = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
208
|
+
p = p.replace(/\/+$/, "");
|
|
209
|
+
if (!/^\/[a-z0-9._-]+(\/[a-z0-9._-]+)*$/i.test(p)) {
|
|
210
|
+
fatal(`Invalid --asset-prefix: ${value}\n Use a path like /cms-assets, or "none" to disable single-origin.`);
|
|
211
|
+
}
|
|
212
|
+
return p;
|
|
213
|
+
}
|
|
214
|
+
|
|
184
215
|
// ─── Pre-flight ──────────────────────────────────────────────────────────────
|
|
185
216
|
|
|
186
217
|
function preflight(flags) {
|
|
@@ -223,9 +254,12 @@ function createPrompt() {
|
|
|
223
254
|
return { ask, close };
|
|
224
255
|
}
|
|
225
256
|
|
|
226
|
-
async function gatherEnv(flags) {
|
|
257
|
+
async function gatherEnv(flags, { singleOrigin }) {
|
|
227
258
|
let dbUrl = flags.db;
|
|
228
|
-
|
|
259
|
+
// In single-origin mode the frontend is the public entry → default :3001.
|
|
260
|
+
// In separate-origins mode the backend is the public CMS URL → default :3000.
|
|
261
|
+
const defaultSiteUrl = singleOrigin ? "http://localhost:3001" : "http://localhost:3000";
|
|
262
|
+
let siteUrl = flags.url ?? defaultSiteUrl;
|
|
229
263
|
let locale = flags.locale ?? "en";
|
|
230
264
|
|
|
231
265
|
if (flags.db && !isValidDbUrl(flags.db)) fatal("--db must start with postgresql:// or postgres://");
|
|
@@ -291,9 +325,47 @@ function writeRootPackageJson(targetDir, dirName) {
|
|
|
291
325
|
"backend:setup": "pnpm --filter ./apps/backend setup",
|
|
292
326
|
},
|
|
293
327
|
engines: { node: ">=20", pnpm: ">=9" },
|
|
294
|
-
|
|
328
|
+
pnpm: {
|
|
329
|
+
// apollo-cms transitively pulls multiple esbuild versions (0.18, 0.25, 0.27).
|
|
330
|
+
// pnpm's binary symlink can pick the wrong platform binary for esbuild's
|
|
331
|
+
// postinstall version check, failing with "Expected X.Y.Z but got A.B.C".
|
|
332
|
+
// Allow listed packages to run their build/postinstall scripts; the rest
|
|
333
|
+
// are skipped (pnpm 10 default is empty allow-list).
|
|
334
|
+
onlyBuiltDependencies: [
|
|
335
|
+
"@swc/core",
|
|
336
|
+
"@swc/core-darwin-arm64",
|
|
337
|
+
"@swc/core-darwin-x64",
|
|
338
|
+
"@swc/core-linux-arm64-gnu",
|
|
339
|
+
"@swc/core-linux-x64-gnu",
|
|
340
|
+
"esbuild",
|
|
341
|
+
"msw",
|
|
342
|
+
"sharp",
|
|
343
|
+
"unrs-resolver",
|
|
344
|
+
"@rolldown/binding-darwin-arm64",
|
|
345
|
+
"@rolldown/binding-linux-x64-gnu",
|
|
346
|
+
"@rolldown/binding-linux-arm64-gnu",
|
|
347
|
+
"better-sqlite3",
|
|
348
|
+
"core-js",
|
|
349
|
+
"core-js-pure",
|
|
350
|
+
],
|
|
351
|
+
},
|
|
295
352
|
};
|
|
296
353
|
writeFileSync(resolve(targetDir, "package.json"), JSON.stringify(pkg, null, 2) + "\n");
|
|
354
|
+
|
|
355
|
+
// .npmrc — keep peer-deps lenient and hoist esbuild's platform binaries so
|
|
356
|
+
// its postinstall version check resolves the correct one across nested
|
|
357
|
+
// dependency trees.
|
|
358
|
+
writeFileSync(
|
|
359
|
+
resolve(targetDir, ".npmrc"),
|
|
360
|
+
[
|
|
361
|
+
"auto-install-peers=true",
|
|
362
|
+
"strict-peer-dependencies=false",
|
|
363
|
+
"public-hoist-pattern[]=*esbuild*",
|
|
364
|
+
"public-hoist-pattern[]=*types*",
|
|
365
|
+
"shell-emulator=true",
|
|
366
|
+
"",
|
|
367
|
+
].join("\n"),
|
|
368
|
+
);
|
|
297
369
|
}
|
|
298
370
|
|
|
299
371
|
function writePnpmWorkspace(targetDir) {
|
|
@@ -321,23 +393,53 @@ function writeRootGitignore(targetDir) {
|
|
|
321
393
|
writeFileSync(resolve(targetDir, ".gitignore"), ignore);
|
|
322
394
|
}
|
|
323
395
|
|
|
324
|
-
function writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret }) {
|
|
396
|
+
function writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret, assetPrefix, backendInternalUrl }) {
|
|
397
|
+
const header = assetPrefix
|
|
398
|
+
? [
|
|
399
|
+
"# Single-origin monorepo dev env",
|
|
400
|
+
"# Public origin = frontend (apps/frontend on :3001). Frontend rewrites /admin/*,",
|
|
401
|
+
"# /api/*, /uploads/*, and the asset prefix to the backend (apps/backend on :3000).",
|
|
402
|
+
]
|
|
403
|
+
: [
|
|
404
|
+
"# Separate-origins monorepo dev env",
|
|
405
|
+
"# Backend runs at http://localhost:3000, frontend at http://localhost:3001.",
|
|
406
|
+
"# To switch to single-origin: set APOLLO_ASSET_PREFIX, BACKEND_INTERNAL_URL,",
|
|
407
|
+
"# NEXT_PUBLIC_APOLLO_ASSET_PREFIX, and add rewrites() to apps/frontend/next.config.ts.",
|
|
408
|
+
];
|
|
409
|
+
|
|
325
410
|
const lines = [
|
|
326
|
-
|
|
411
|
+
...header,
|
|
412
|
+
"",
|
|
413
|
+
"# ── Shared (consumed by both apps) ───────────────────────────────",
|
|
327
414
|
`DATABASE_URL=${dbUrl}`,
|
|
328
415
|
`APOLLO_SECRET=${authSecret}`,
|
|
329
|
-
`BETTER_AUTH_SECRET=${authSecret}`,
|
|
330
416
|
`NEXT_PUBLIC_SITE_URL=${siteUrl}`,
|
|
331
417
|
`NEXT_PUBLIC_DEFAULT_LOCALE=${locale}`,
|
|
332
418
|
"",
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
419
|
+
];
|
|
420
|
+
|
|
421
|
+
if (assetPrefix) {
|
|
422
|
+
lines.push(
|
|
423
|
+
"# ── Backend (apps/backend) ───────────────────────────────────────",
|
|
424
|
+
`APOLLO_ASSET_PREFIX=${assetPrefix}`,
|
|
425
|
+
"",
|
|
426
|
+
"# ── Frontend (apps/frontend) ─────────────────────────────────────",
|
|
427
|
+
`BACKEND_INTERNAL_URL=${backendInternalUrl}`,
|
|
428
|
+
`NEXT_PUBLIC_APOLLO_ASSET_PREFIX=${assetPrefix}`,
|
|
429
|
+
"",
|
|
430
|
+
);
|
|
431
|
+
} else {
|
|
432
|
+
lines.push(
|
|
433
|
+
"# ── Frontend (apps/frontend) ─────────────────────────────────────",
|
|
434
|
+
`NEXT_PUBLIC_BACKEND_URL=${siteUrl}`,
|
|
435
|
+
"",
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
writeFileSync(resolve(targetDir, ".env.local"), lines.join("\n"));
|
|
338
440
|
}
|
|
339
441
|
|
|
340
|
-
function writeFrontendApp(targetDir, frontendName, siteUrl) {
|
|
442
|
+
function writeFrontendApp(targetDir, frontendName, siteUrl, assetPrefix, backendInternalUrl) {
|
|
341
443
|
const dir = resolve(targetDir, FRONTEND_PATH);
|
|
342
444
|
mkdirSync(dir, { recursive: true });
|
|
343
445
|
mkdirSync(resolve(dir, "src/app"), { recursive: true });
|
|
@@ -397,15 +499,70 @@ function writeFrontendApp(targetDir, frontendName, siteUrl) {
|
|
|
397
499
|
) + "\n",
|
|
398
500
|
);
|
|
399
501
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
502
|
+
// Frontend Next.js config:
|
|
503
|
+
// - Single-origin mode (assetPrefix set): rewrite all backend paths.
|
|
504
|
+
// - Separate-origins mode (assetPrefix empty): no rewrites; the two apps
|
|
505
|
+
// are reachable on their own ports/subdomains.
|
|
506
|
+
const nextConfigContent = assetPrefix
|
|
507
|
+
? `import type { NextConfig } from "next";
|
|
508
|
+
|
|
509
|
+
const BACKEND = process.env.BACKEND_INTERNAL_URL ?? "http://localhost:3000";
|
|
510
|
+
const ASSET_PREFIX = process.env.NEXT_PUBLIC_APOLLO_ASSET_PREFIX ?? "${assetPrefix}";
|
|
511
|
+
|
|
512
|
+
const config: NextConfig = {
|
|
513
|
+
reactStrictMode: true,
|
|
514
|
+
async rewrites() {
|
|
515
|
+
return [
|
|
516
|
+
// Apollo CMS admin UI
|
|
517
|
+
{ source: "/admin/:path*", destination: \`\${BACKEND}/admin/:path*\` },
|
|
518
|
+
// Apollo CMS APIs (REST + auth + cron + email + health + mcp + …)
|
|
519
|
+
{ source: "/api/auth/:path*", destination: \`\${BACKEND}/api/auth/:path*\` },
|
|
520
|
+
{ source: "/api/v1/:path*", destination: \`\${BACKEND}/api/v1/:path*\` },
|
|
521
|
+
{ source: "/api/cron", destination: \`\${BACKEND}/api/cron\` },
|
|
522
|
+
{ source: "/api/email/:path*", destination: \`\${BACKEND}/api/email/:path*\` },
|
|
523
|
+
{ source: "/api/health", destination: \`\${BACKEND}/api/health\` },
|
|
524
|
+
{ source: "/api/mcp", destination: \`\${BACKEND}/api/mcp\` },
|
|
525
|
+
{ source: "/api/editing-presence/:path*", destination: \`\${BACKEND}/api/editing-presence/:path*\` },
|
|
526
|
+
{ source: "/api/admin/:path*", destination: \`\${BACKEND}/api/admin/:path*\` },
|
|
527
|
+
// Media uploads
|
|
528
|
+
{ source: "/uploads/:path*", destination: \`\${BACKEND}/uploads/:path*\` },
|
|
529
|
+
// Sentry tunnel (no-op if SENTRY_DSN isn't set on the backend)
|
|
530
|
+
{ source: "/monitoring", destination: \`\${BACKEND}/monitoring\` },
|
|
531
|
+
// Backend's namespaced Next.js chunks (set via APOLLO_ASSET_PREFIX)
|
|
532
|
+
{ source: \`\${ASSET_PREFIX}/:path*\`, destination: \`\${BACKEND}\${ASSET_PREFIX}/:path*\` },
|
|
533
|
+
];
|
|
534
|
+
},
|
|
535
|
+
};
|
|
404
536
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
`
|
|
408
|
-
|
|
537
|
+
export default config;
|
|
538
|
+
`
|
|
539
|
+
: `import type { NextConfig } from "next";
|
|
540
|
+
|
|
541
|
+
// Separate-origins mode: backend runs at http://localhost:3000, frontend here.
|
|
542
|
+
// Switch to single-origin by setting APOLLO_ASSET_PREFIX in apps/backend/.env.local
|
|
543
|
+
// and adding rewrites() — see this monorepo's README for the recipe.
|
|
544
|
+
const config: NextConfig = {
|
|
545
|
+
reactStrictMode: true,
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
export default config;
|
|
549
|
+
`;
|
|
550
|
+
writeFileSync(resolve(dir, "next.config.ts"), nextConfigContent);
|
|
551
|
+
|
|
552
|
+
const envLocalLines = assetPrefix
|
|
553
|
+
? [
|
|
554
|
+
`# Internal backend URL (used by Next.js rewrites — not exposed to browser)`,
|
|
555
|
+
`BACKEND_INTERNAL_URL=${backendInternalUrl}`,
|
|
556
|
+
`# Mirror of backend's APOLLO_ASSET_PREFIX`,
|
|
557
|
+
`NEXT_PUBLIC_APOLLO_ASSET_PREFIX=${assetPrefix}`,
|
|
558
|
+
"",
|
|
559
|
+
]
|
|
560
|
+
: [
|
|
561
|
+
`# Public URL of the backend (used by your client code)`,
|
|
562
|
+
`NEXT_PUBLIC_BACKEND_URL=${siteUrl}`,
|
|
563
|
+
"",
|
|
564
|
+
];
|
|
565
|
+
writeFileSync(resolve(dir, ".env.local.example"), envLocalLines.join("\n"));
|
|
409
566
|
|
|
410
567
|
writeFileSync(
|
|
411
568
|
resolve(dir, "src/app/layout.tsx"),
|
|
@@ -414,16 +571,77 @@ function writeFrontendApp(targetDir, frontendName, siteUrl) {
|
|
|
414
571
|
|
|
415
572
|
writeFileSync(
|
|
416
573
|
resolve(dir, "src/app/page.tsx"),
|
|
417
|
-
`export default function Page() {\n return (\n <main style={{ padding: 40, fontFamily: "system-ui" }}>\n <h1>Frontend</h1>\n <p
|
|
574
|
+
`export default function Page() {\n return (\n <main style={{ padding: 40, fontFamily: "system-ui" }}>\n <h1>Frontend</h1>\n <p>\n Admin: <a href="/admin">/admin</a>\n </p>\n </main>\n );\n}\n`,
|
|
418
575
|
);
|
|
419
576
|
|
|
420
577
|
writeFileSync(
|
|
421
578
|
resolve(dir, ".gitignore"),
|
|
422
579
|
["node_modules", ".next", "dist", ".env.local", ""].join("\n"),
|
|
423
580
|
);
|
|
581
|
+
|
|
582
|
+
// Vercel project config for the frontend. Skip cron + region pinning is
|
|
583
|
+
// optional; configure Root Directory + "Include all submodules" in the
|
|
584
|
+
// Vercel UI when linking the project.
|
|
585
|
+
const vercelJson = {
|
|
586
|
+
$schema: "https://openapi.vercel.sh/vercel.json",
|
|
587
|
+
regions: ["sin1"],
|
|
588
|
+
};
|
|
589
|
+
writeFileSync(resolve(dir, "vercel.json"), JSON.stringify(vercelJson, null, 2) + "\n");
|
|
590
|
+
|
|
591
|
+
writeFileSync(
|
|
592
|
+
resolve(dir, ".vercelignore"),
|
|
593
|
+
["node_modules", ".next", ".env.local", ""].join("\n"),
|
|
594
|
+
);
|
|
424
595
|
}
|
|
425
596
|
|
|
426
|
-
function writeReadme(targetDir, dirName, frontendName) {
|
|
597
|
+
function writeReadme(targetDir, dirName, frontendName, assetPrefix) {
|
|
598
|
+
const singleOrigin = !!assetPrefix;
|
|
599
|
+
|
|
600
|
+
const originSection = singleOrigin
|
|
601
|
+
? `## Routing model — single origin
|
|
602
|
+
|
|
603
|
+
Both apps share one public origin (the frontend). The frontend rewrites these
|
|
604
|
+
paths to the backend so /_next/* doesn't collide:
|
|
605
|
+
|
|
606
|
+
| Path | Goes to |
|
|
607
|
+
| --------------------------- | ------------------ |
|
|
608
|
+
| \`/\` and other frontend routes | \`apps/frontend\` |
|
|
609
|
+
| \`/admin/*\` | \`apps/backend\` |
|
|
610
|
+
| \`/api/auth/*\`, \`/api/v1/*\`, \`/api/email/*\`, \`/api/cron\`, \`/api/health\`, \`/api/mcp\`, \`/api/admin/*\`, \`/api/editing-presence/*\` | \`apps/backend\` |
|
|
611
|
+
| \`/uploads/*\` | \`apps/backend\` (media) |
|
|
612
|
+
| \`${assetPrefix}/*\` | \`apps/backend\` (chunks via \`APOLLO_ASSET_PREFIX\`) |
|
|
613
|
+
|
|
614
|
+
The frontend MUST NOT define routes at \`/admin\`, \`/api/auth\`, \`/api/v1\`, etc.
|
|
615
|
+
If you need your own API, namespace it under \`/api/internal/*\` or similar.
|
|
616
|
+
|
|
617
|
+
To **disable** single-origin and run the backend on its own subdomain,
|
|
618
|
+
delete \`APOLLO_ASSET_PREFIX\` from \`apps/backend/.env.local\` and remove the
|
|
619
|
+
\`rewrites()\` block from \`apps/frontend/next.config.ts\`.
|
|
620
|
+
|
|
621
|
+
### Known limitations
|
|
622
|
+
|
|
623
|
+
- The backend's \`/_next/image\` (Next.js Image optimization endpoint) is **not**
|
|
624
|
+
rewritten — only \`/_next/static\` moves under \`APOLLO_ASSET_PREFIX\`. If your
|
|
625
|
+
backend admin uses \`<Image>\` to render \`/uploads/*\` media, those will fall
|
|
626
|
+
back to the frontend's image optimizer and 404. Apollo CMS's stock admin uses
|
|
627
|
+
plain \`<img>\` for uploaded media so this rarely matters; if you customize the
|
|
628
|
+
admin and need optimized images, switch to separate-origins mode or set
|
|
629
|
+
\`unoptimized\` on those \`<Image>\` instances.
|
|
630
|
+
`
|
|
631
|
+
: `## Routing model — separate origins
|
|
632
|
+
|
|
633
|
+
The two apps run on separate origins. Default ports:
|
|
634
|
+
|
|
635
|
+
- Backend (apps/backend): http://localhost:3000
|
|
636
|
+
- Frontend (apps/frontend): http://localhost:3001
|
|
637
|
+
|
|
638
|
+
\`NEXT_PUBLIC_SITE_URL\` should point at whichever origin you treat as the
|
|
639
|
+
public CMS URL (typically the backend). To switch to **single-origin** later,
|
|
640
|
+
re-run the installer with \`--asset-prefix /cms-assets\` or set
|
|
641
|
+
\`APOLLO_ASSET_PREFIX\` in \`apps/backend/.env.local\` and add the matching
|
|
642
|
+
\`rewrites()\` block to \`apps/frontend/next.config.ts\`.
|
|
643
|
+
`;
|
|
644
|
+
|
|
427
645
|
const readme = `# ${dirName}
|
|
428
646
|
|
|
429
647
|
Monorepo with a custom frontend and Apollo CMS as a git submodule backend.
|
|
@@ -447,6 +665,7 @@ pnpm backend:setup # push schema + seed apollo-cms
|
|
|
447
665
|
pnpm dev # runs frontend (3001) + backend (3000) in parallel
|
|
448
666
|
\`\`\`
|
|
449
667
|
|
|
668
|
+
${originSection}
|
|
450
669
|
## Updating the backend
|
|
451
670
|
|
|
452
671
|
Apollo CMS is tracked as a git submodule. Pull the latest:
|
|
@@ -462,6 +681,71 @@ Do **not** edit files inside \`apps/backend\`. Open issues / PRs in the
|
|
|
462
681
|
|
|
463
682
|
Shared dev env lives in the root \`.env.local\`. The backend reads its own
|
|
464
683
|
\`apps/backend/.env.local\` (already populated by the installer).
|
|
684
|
+
|
|
685
|
+
## Deploy on Vercel
|
|
686
|
+
|
|
687
|
+
Two Vercel projects, one repo. Each project picks up its own Root Directory.
|
|
688
|
+
|
|
689
|
+
### 1) Backend project
|
|
690
|
+
|
|
691
|
+
- **Import** this repo into Vercel as a new project.
|
|
692
|
+
- **Root Directory**: \`apps/backend\`
|
|
693
|
+
- **Build Command**: \`cd ../.. && pnpm install --frozen-lockfile && pnpm --filter ./apps/backend build\`
|
|
694
|
+
- **Install Command**: leave empty (handled in build)
|
|
695
|
+
- **Settings → Git → Include all submodules: ON** (Vercel checks out an empty \`apps/backend\` otherwise)
|
|
696
|
+
- **Environment variables**:
|
|
697
|
+
\`\`\`
|
|
698
|
+
DATABASE_URL=postgresql://…
|
|
699
|
+
APOLLO_SECRET=<openssl rand -hex 32>
|
|
700
|
+
NEXT_PUBLIC_SITE_URL=https://yourdomain.com # the PUBLIC origin
|
|
701
|
+
NEXT_PUBLIC_DEFAULT_LOCALE=en
|
|
702
|
+
${singleOrigin ? `APOLLO_ASSET_PREFIX=${assetPrefix}` : "# APOLLO_ASSET_PREFIX=/cms-assets # only when single-origin"}
|
|
703
|
+
CRON_SECRET=<random> # protects /api/cron
|
|
704
|
+
# Storage on Vercel cannot use local FS — pick one:
|
|
705
|
+
# Vercel Blob: BLOB_READ_WRITE_TOKEN=…
|
|
706
|
+
# S3 / R2 / Spaces: S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_BUCKET, S3_REGION, S3_ENDPOINT
|
|
707
|
+
APOLLO_DISABLE_LOCAL_STORAGE=1
|
|
708
|
+
\`\`\`
|
|
709
|
+
- **Cron**: \`apps/backend/vercel.json\` declares \`/api/cron\` on a 5-minute schedule (from apollo-cms upstream).
|
|
710
|
+
|
|
711
|
+
### 2) Frontend project
|
|
712
|
+
|
|
713
|
+
- **Import the same repo** as a separate Vercel project.
|
|
714
|
+
- **Root Directory**: \`apps/frontend\`
|
|
715
|
+
- **Build Command**: \`cd ../.. && pnpm install --frozen-lockfile && pnpm --filter ./apps/frontend build\`
|
|
716
|
+
- **Install Command**: leave empty
|
|
717
|
+
- **Settings → Git → Include all submodules: ON**
|
|
718
|
+
- **Environment variables**:
|
|
719
|
+
\`\`\`
|
|
720
|
+
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
|
|
721
|
+
${singleOrigin
|
|
722
|
+
? ` BACKEND_INTERNAL_URL=https://<your-backend>.vercel.app\n NEXT_PUBLIC_APOLLO_ASSET_PREFIX=${assetPrefix}`
|
|
723
|
+
: ` NEXT_PUBLIC_BACKEND_URL=https://<your-backend>.vercel.app`
|
|
724
|
+
}
|
|
725
|
+
\`\`\`
|
|
726
|
+
|
|
727
|
+
### 3) Custom domain
|
|
728
|
+
|
|
729
|
+
Attach \`yourdomain.com\` to the **frontend** project${singleOrigin ? " (in single-origin mode it's the public entry point)" : ""}. The backend stays on its \`*.vercel.app\` URL${singleOrigin ? " — that's what the rewrite proxies to" : ""}.
|
|
730
|
+
|
|
731
|
+
### 4) Skip duplicate builds (optional)
|
|
732
|
+
|
|
733
|
+
Each push triggers both projects to rebuild. Add an **Ignored Build Step** in
|
|
734
|
+
each project's Settings → Git:
|
|
735
|
+
|
|
736
|
+
- **Backend**: \`git diff HEAD^ HEAD --quiet -- apps/backend\` (exits 0 → skip build)
|
|
737
|
+
- **Frontend**: \`git diff HEAD^ HEAD --quiet -- apps/frontend\`
|
|
738
|
+
|
|
739
|
+
### Gotchas
|
|
740
|
+
|
|
741
|
+
- **Submodule must be initialized** on Vercel — the "Include all submodules"
|
|
742
|
+
toggle is the most common reason builds fail with a missing \`apps/backend\`.
|
|
743
|
+
- **OAuth callbacks** for email providers must use the public domain:
|
|
744
|
+
\`https://yourdomain.com/api/email/oauth/callback\`.${singleOrigin ? " The frontend rewrite forwards it to the backend." : ""}
|
|
745
|
+
- **Cron** runs on the backend project only. Vercel sends \`Authorization:
|
|
746
|
+
Bearer $CRON_SECRET\` automatically when \`CRON_SECRET\` is set.${singleOrigin ? `
|
|
747
|
+
- **Better Auth** uses \`trustedProxyHeaders: true\` so the rewrite proxy's
|
|
748
|
+
\`x-forwarded-host\` lands cookies at the public origin without extra config.` : ""}
|
|
465
749
|
`;
|
|
466
750
|
writeFileSync(resolve(targetDir, "README.md"), readme);
|
|
467
751
|
}
|
|
@@ -490,9 +774,13 @@ async function main() {
|
|
|
490
774
|
|
|
491
775
|
// ── Step 2: Gather env ──
|
|
492
776
|
step(2, "Configuring environment");
|
|
493
|
-
const
|
|
777
|
+
const assetPrefix = normalizeAssetPrefix(flags.assetPrefix);
|
|
778
|
+
const singleOrigin = !!assetPrefix;
|
|
779
|
+
const { dbUrl, siteUrl, locale } = await gatherEnv(flags, { singleOrigin });
|
|
494
780
|
const authSecret = randomBytes(48).toString("base64");
|
|
781
|
+
const backendInternalUrl = "http://localhost:3000";
|
|
495
782
|
success(`Frontend pkg name: ${frontendName}`);
|
|
783
|
+
success(`Asset prefix: ${assetPrefix || "(disabled — separate origins)"}`);
|
|
496
784
|
|
|
497
785
|
// ── Step 3: Scaffold root ──
|
|
498
786
|
step(3, "Scaffolding monorepo root");
|
|
@@ -501,8 +789,8 @@ async function main() {
|
|
|
501
789
|
writeRootPackageJson(targetDir, dirName);
|
|
502
790
|
writePnpmWorkspace(targetDir);
|
|
503
791
|
writeRootGitignore(targetDir);
|
|
504
|
-
writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret });
|
|
505
|
-
writeReadme(targetDir, dirName, frontendName);
|
|
792
|
+
writeRootEnv(targetDir, { dbUrl, siteUrl, locale, authSecret, assetPrefix, backendInternalUrl });
|
|
793
|
+
writeReadme(targetDir, dirName, frontendName, assetPrefix);
|
|
506
794
|
success("package.json, pnpm-workspace.yaml, .gitignore, .env.local, README.md");
|
|
507
795
|
|
|
508
796
|
// ── Step 4: git init ──
|
|
@@ -512,7 +800,7 @@ async function main() {
|
|
|
512
800
|
|
|
513
801
|
// ── Step 5: Frontend skeleton ──
|
|
514
802
|
step(5, "Creating frontend app");
|
|
515
|
-
writeFrontendApp(targetDir, frontendName, siteUrl);
|
|
803
|
+
writeFrontendApp(targetDir, frontendName, siteUrl, assetPrefix, backendInternalUrl);
|
|
516
804
|
success(`apps/frontend (${frontendName})`);
|
|
517
805
|
|
|
518
806
|
// ── Step 6: Backend submodule ──
|
|
@@ -537,15 +825,17 @@ async function main() {
|
|
|
537
825
|
}
|
|
538
826
|
|
|
539
827
|
// Mirror env to backend
|
|
540
|
-
const
|
|
828
|
+
const backendEnvLines = [
|
|
541
829
|
`DATABASE_URL=${dbUrl}`,
|
|
542
830
|
`APOLLO_SECRET=${authSecret}`,
|
|
543
|
-
`BETTER_AUTH_SECRET=${authSecret}`,
|
|
544
831
|
`NEXT_PUBLIC_SITE_URL=${siteUrl}`,
|
|
545
832
|
`NEXT_PUBLIC_DEFAULT_LOCALE=${locale}`,
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
833
|
+
];
|
|
834
|
+
if (assetPrefix) {
|
|
835
|
+
backendEnvLines.push(`APOLLO_ASSET_PREFIX=${assetPrefix}`);
|
|
836
|
+
}
|
|
837
|
+
backendEnvLines.push("");
|
|
838
|
+
writeFileSync(resolve(targetDir, BACKEND_PATH, ".env.local"), backendEnvLines.join("\n"));
|
|
549
839
|
success("apps/backend/.env.local");
|
|
550
840
|
}
|
|
551
841
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-apollo-monorepo",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Scaffold a monorepo with a frontend app and Apollo CMS as a git submodule backend",
|
|
5
|
-
"bin":
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Scaffold a monorepo with a frontend app and Apollo CMS as a git submodule backend (single-origin via Next.js rewrites + assetPrefix)",
|
|
5
|
+
"bin": {
|
|
6
|
+
"create-apollo-monorepo": "index.mjs"
|
|
7
|
+
},
|
|
6
8
|
"type": "module",
|
|
7
9
|
"engines": {
|
|
8
10
|
"node": ">=20"
|